本文将详细介绍在spring boot环境中使用spring security实现token认证的方法和注意事项。

Maven依赖

		<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>

Spring Security 核心类

WebSecurityConfigurerAdapter

要利用Spring Security把安全的事情做了,最核心的就是继承这个WebSecurityConfigurerAdapter,根据自己的业务需要重新configure方法,示例代码如下:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;

@Autowired
private UserDetailsService userDetailsService;

@Value("${anonymous.path}")
private String[] ANONYMOUS_PATH;

@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}

@Override
public void configure(WebSecurity web) throws Exception {
if (ANONYMOUS_PATH.length > 0) {
web.ignoring().antMatchers(ANONYMOUS_PATH);
}
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// 由于使用的是JWT,我们这里不需要csrf
.csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
// 允许对于网站静态资源的无授权访问
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/layui/**",
"/dist/**",
"/**/*.js"
).permitAll()
//允许ajax的option请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 对于获取token的rest api要允许匿名访问
.antMatchers(
"/auth/login",
"/swagger-ui.html",
"/webjars/**",
"/swagger-resources/**",
"/*/api-docs",
"/configuration/**",
"/attachment/**",
"/system/encrypt",
"/actuator/**",
"/h2/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();


// 添加JWT filter
httpSecurity
.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

// 禁用缓存
httpSecurity.headers().cacheControl();
//允许跨域
httpSecurity.headers().frameOptions().disable();
}


}
  • configure(HttpSecurity httpSecurity)里面的注释很全,主要做的事情是配置规则和参数。
  • configure(WebSecurity web)的目的是实现自定义的匿名访问授权。

OncePerRequestFilter

上面的配置类中声明了要使用authenticationTokenFilterBean进行权限验证,下面这个继承OncePerRequestFilter就是它的具体实现,示例如下:

@SuppressWarnings("SpringJavaAutowiringInspection")
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired
private JwtUserDetailsService jwtUserDetailsService;

@Autowired
private UserService userService;


private String tokenHeader = "Authorization";

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String authToken = request.getHeader(this.tokenHeader);
if (authToken != null) {
String usercode = TokenUtils.getUsercodeFromToken(authToken);
logger.info("checking authentication " + usercode);
if (usercode != null && SecurityContextHolder.getContext().getAuthentication() == null) {
SystemUserModel user = userService.getUserByUsercode(usercode);
UserDetails userDetails = jwtUserDetailsService.loadUserByUsername(usercode);

if (TokenUtils.validateToken(authToken, userDetails) && userService.isPermissionApi(user.getId(), request.getRequestURI(), request.getMethod())) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(
request));
logger.info("authenticated user " + usercode + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
chain.doFilter(request, response);
}
}
  • doFilterInternal是核心方法,在这里实现了token的提取和认证

UserDetailsService

UserDetailsService是Spring Security获取用户信息的核心类,配合使用的model是UserDetails,用这个类去存放用户信息。

@Service
public class JwtUserDetailsService implements UserDetailsService {

private UserService userService;

@Override
public UserDetails loadUserByUsername(String usercode) throws UsernameNotFoundException {
SystemUserModel user = userService.getUserByUsercode(usercode);
if (user == null) {
throw new UsernameNotFoundException(String.format("No user found with username '%s'.", usercode));
} else {
String role = userService.getRole(usercode).getRolename();
return JwtUserFactory.create(user, role);
}
}

@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
}
  • loadUserByUsername是需要重写的方法,在这里写明如何加载用户

UserDetails

下面是UserDetails的一个示例,要点就是要实现UserDetails里的接口

public class JwtUser implements UserDetails {
private final String id;
private final String username;
private final String password;
private final String email;
private final Collection<? extends GrantedAuthority> authorities;

public JwtUser(
String id,
String username,
String password,
String email,
Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.email = email;
this.authorities = authorities;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@JsonIgnore
public String getId() {
return id;
}

@JsonIgnore
@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return username;
}

@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}

@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}

@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}

@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}

Token 核心类

token这块的重点就是如何生成token,如何验证token,如何刷新token。这些可以封装成一个工具类。如下:

@Component
public class TokenUtils {

public static final String CLAIM_KEY_USERNAME = "sub";
public static final String CLAIM_KEY_CREATED = "crt";

private static String secret;

private static Long expiration;

/**
* 从 Token 中获取 usercode
*/
public static String getUsercodeFromToken(String token) {
String usercode;
try {
final Claims claims = getClaimsFromToken(token);
usercode = claims.getSubject();
} catch (Exception e) {
usercode = e.toString();
}
return usercode;
}


/**
* 从 Token 中获取创建时间
*/
public static Date getCreatedDateFromToken(String token) {
Date created;
try {
final Claims claims = getClaimsFromToken(token);
created = Objects.equals(null, claims) ? null : new Date((Long) claims.get(CLAIM_KEY_CREATED));
} catch (Exception e) {
created = null;
}
return created;
}

/**
* 从 Token 中获取过时时间
*/
public static Date getExpirationDateFromToken(String token) {
Date expiration;
try {
final Claims claims = getClaimsFromToken(token);
expiration = claims.getExpiration();
} catch (Exception e) {
expiration = null;
}
return expiration;
}

private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}

/**
* 生成过期时间
*/
private static Date generateExpirationDate() {
return new Date(System.currentTimeMillis() + expiration * 1000);
}

/**
* 验证 Token 是否过期
*/
private static Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}

private static Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
return (lastPasswordReset != null && created.before(lastPasswordReset));
}

/**
* 生成 Token
*/
public static String generateToken(String usercode) {
Map<String, Object> claims = new HashMap<>();
claims.put(CLAIM_KEY_USERNAME, usercode);
claims.put(CLAIM_KEY_CREATED, new Date());
return generateToken(claims);
}

/**
* 生成 Token
*/
public static String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims)
.setExpiration(generateExpirationDate())
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}

public static Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) {
final Date created = getCreatedDateFromToken(token);
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset)
&& !isTokenExpired(token);
}

/**
* 刷新 Token
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
final Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}

/**
* 验证 Token
*/
public static Boolean validateToken(String token,SystemUserModel user) {
final String usercode = getUsercodeFromToken(token);
return (usercode.equals(user.getUsercode()) && !isTokenExpired(token));
}

public static Boolean validateToken(String token,UserDetails userDetails) {
final String usercode = getUsercodeFromToken(token);
return (usercode.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

@Value("${jwt.secret}")
public void setSecret(String secret) {
TokenUtils.secret = secret;
}

@Value("${jwt.expiration}")
public void setExpiration(Long expiration) {
TokenUtils.expiration = expiration;
}
}

END