SpringBoot SpringSecurity JWT实现系统安全策略详解
Buckletime 人气:0security进行用户验证和授权;jwt负责颁发令牌与校验,判断用户登录状态
一、原理
1. SpringSecurity过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。
- SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中。
- LogoutFilter:用于处理退出登录。
- UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。
- BasicAuthenticationFilter:检测和处理 http basic 认证。
- ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
- FilterSecurityInterceptor:可以看做过滤器链的出口。
- …
流程说明:客户端发起一个请求,进入 Security 过滤器链。
当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
2. JWT校验
首先前端一样是把登录信息发送给后端,后端查询数据库校验用户的账号和密码是否正确,正确的话则使用jwt生成token,并且返回给前端。以后前端每次请求时,都需要携带token,后端获取token后,使用jwt进行验证用户的token是否无效或过期,验证成功后才去做相应的逻辑。
二、Security+JWT配置说明
1. 添加maven依赖
<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配置
/** * 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校验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生成与解析工具类
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()); } }
加载全部内容