SpringBoot + Security + JWT security policy
Security performs user verification and authorization; jwt is responsible for issuing tokens and verification, and judging user login status
1. Principle
1. Spring Security filter chain
Spring Security adopts the chain of responsibility design pattern, which has a long filter chain.
- SecurityContextPersistenceFilter: Before each request is processed, the security context information related to the request is loaded into the SecurityContextHolder.
- LogoutFilter: Used to handle logout.
- UsernamePasswordAuthenticationFilter: Used to process form-based login requests and obtain usernames and passwords from the form.
- BasicAuthenticationFilter: Detect and handle http basic authentication.
- ExceptionTranslationFilter: Handles AccessDeniedException and AuthenticationException exceptions.
- FilterSecurityInterceptor: It can be regarded as the exit of the filter chain.
- …
Process Description: The client initiates a request and enters the Security filter chain.
When going to LogoutFilter, judge whether it is a logout path. If it is a logout path, go to logoutHandler. If the logout is successful, go to logoutSuccessHandler. If the logout fails, use ExceptionTranslationFilter; if it is not a logout path, go directly to the next a filter.
When going to UsernamePasswordAuthenticationFilter, judge whether it is a login path. If yes, enter the filter for login operation. If the login fails, go to AuthenticationFailureHandler for login failure processing. If login is successful, go to AuthenticationSuccessHandler for login success processing. If it is not login Requests do not enter this filter.
When going to FilterSecurityInterceptor, you will get the uri, and find the corresponding authentication manager according to the uri. The authentication manager will do the authentication work. If the authentication is successful, it will go to the Controller layer; otherwise, it will go to the AccessDeniedHandler authentication failure processor for processing.
2. JWT verification
First of all, the front-end also sends the login information to the back-end, and the back-end queries the database to verify whether the user's account and password are correct. If it is correct, it uses jwt to generate a token and returns it to the front-end. In the future, each time the front-end requests, it needs to carry the token. After the back-end obtains the token, it uses jwt to verify whether the user's token is invalid or expired. After the verification is successful, it does the corresponding logic.
2. Security + JWT configuration instructions
1. Add maven dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2. securityConfig configuration
/**
* Security 配置
*/
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
JWTLogoutSuccessHandler jwtLogoutSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Value("${security.enable}")
private Boolean securityIs = Boolean.TRUE;
@Value("${security.permit}")
private String permit;
@Bean
public HttpFirewall allowUrlEncodedSlashHttpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
//此处可添加别的规则,目前只设置 允许双 //
firewall.setAllowUrlEncodedDoubleSlash(true);
return firewall;
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager(), jwtAuthenticationEntryPoint);
return jwtAuthenticationFilter;
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.cors().and().csrf().disable()
// 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(permit.split(",")).permitAll();
if (!securityIs) {
http.authorizeRequests().antMatchers("/**").permitAll();
}
registry.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
// 验证码过滤器放在UsernamePassword过滤器之前
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
}
3. JwtAuthenticationFilter check token
package cn.piesat.gf.filter;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import cn.piesat.gf.dao.user.SysUserDao;
import cn.piesat.gf.model.entity.user.SysUser;
import cn.piesat.gf.exception.ExpiredAuthenticationException;
import cn.piesat.gf.exception.MyAuthenticationException;
import cn.piesat.gf.service.user.impl.UserDetailServiceImpl;
import cn.piesat.gf.utils.Constants;
import cn.piesat.gf.utils.JwtUtils;
import cn.piesat.gf.utils.Result;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
@Autowired
JwtUtils jwtUtils;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
SysUserDao sysUserRepository;
@Autowired
RedisTemplate redisTemplate;
@Value("${security.single}")
private Boolean singleLogin = false;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
super(authenticationManager, authenticationEntryPoint);
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
this.authenticationManager = authenticationManager;
this.authenticationEntryPoint = authenticationEntryPoint;
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtils.getHeader());
// 这里如果没有jwt,继续往后走,因为后面还有鉴权管理器等去判断是否拥有身份凭证,所以是可以放行的
// 没有jwt相当于匿名访问,若有一些接口是需要权限的,则不能访问这些接口
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
try {
Claims claim = jwtUtils.getClaimsByToken(jwt);
if (claim == null) {
throw new MyAuthenticationException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new MyAuthenticationException("token 已过期");
}
String username = claim.getSubject();
Object o1 = redisTemplate.opsForValue().get(Constants.TOKEN_KEY + username);
String o = null;
if(!ObjectUtils.isEmpty(o1)){
o = o1.toString();
}
if (!StringUtils.hasText(o)) {
throw new ExpiredAuthenticationException("您的登录信息已过期,请重新登录!");
}
if (singleLogin && StringUtils.hasText(o) && !jwt.equals(o)) {
throw new MyAuthenticationException("您的账号已别处登录,您已下线,如有异常请及时修改密码!");
}
// 获取用户的权限等信息
SysUser sysUser = sysUserRepository.findByUserName(username);
// 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getUserId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
} catch (AuthenticationException e) {
log.error(ExceptionUtil.stacktraceToString(e));
authenticationEntryPoint.commence(request, response, e);
return;
} catch (Exception e){
log.error(ExceptionUtil.stacktraceToString(e));
response.getOutputStream().write(JSONUtil.toJsonStr(Result.fail(e.getMessage())).getBytes(StandardCharsets.UTF_8));
response.getOutputStream().flush();
response.getOutputStream().close();
return;
}
}
}
4. JWT generation and parsing tools
package cn.piesat.gf.utils;
import cn.hutool.core.exceptions.ExceptionUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
@Data
@Component
@ConfigurationProperties(prefix = "jwt.config")
@Slf4j
public class JwtUtils {
private long expire;
private String secret;
private String header;
// 生成JWT
public String generateToken(String username) {
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
// 解析JWT
public Claims getClaimsByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
log.error(ExceptionUtil.stacktraceToString(e));
return null;
}
}
// 判断JWT是否过期
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}