SpringBoot SpringSecurity JWT实现系统安全策略详解
作者:Buckletime
security进行用户验证和授权;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()); } }
到此这篇关于SpringBoot SpringSecurity JWT实现系统安全策略详解的文章就介绍到这了,更多相关SpringBoot SpringSecurity JWT内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- SpringBoot整合SpringSecurity和JWT和Redis实现统一鉴权认证
- SpringBoot+SpringSecurity+JWT实现系统认证与授权示例
- SpringBoot+SpringSecurity+jwt实现验证
- SpringBoot整合SpringSecurity实现JWT认证的项目实践
- springboot+jwt+springSecurity微信小程序授权登录问题
- Springboot+SpringSecurity+JWT实现用户登录和权限认证示例
- SpringBoot集成SpringSecurity和JWT做登陆鉴权的实现
- 详解SpringBoot+SpringSecurity+jwt整合及初体验
- SpringBoot3.0+SpringSecurity6.0+JWT的实现