SpringSecurity整合jwt权限认证的全流程讲解

JWT 本文代码截取自实际项目。
【SpringSecurity整合jwt权限认证的全流程讲解】jwt(Json Web Token),一个token,令牌。
简单流程:
用户登录成功后,后端返回一个token,也就是颁发给用户一个凭证。之后每一次访问,前端都需要携带这个token,后端通过token来解析出当前访问对象。
优点
1、一定程度上解放了后端,后端不需要再记录当前用户是谁,不需要再维护一个session,节省了开销。
2、session依赖于cookie,某些场合cookie是用不了的,比如用户浏览器cookie被禁用、移动端无法存储cookie等。
缺点
1、既然服务器通过token就可以知道当前用户是谁,说明其中包含了用户信息,有一定的泄露风险。
2、因为是无状态的,服务器不维持会话,那么每一次请求都会重新去数据库读取权限信息,性能有一定影响。
(如果想提高性能,可以将权限数据存到redis,但是如果用redis,就已经失去了jwt的优点,直接用普通的token+redis即可)
3、只能校验token是否正确,通过设定过期时间来确定其有效性,不可以手动注销。
先说怎么做,再说为什么。
代码 依赖

org.springframework.bootspring-boot-starter-security2.3.3.RELEASE io.jsonwebtokenjjwt0.9.1

jwt工具类
public class JwtTokenUtils implements Serializable { private static final long serialVersionUID = -3369436201465044824L; //生成tokenpublic static String createToken(String username) {return Jwts.builder().setSubject(username).setExpiration(new Date(System.currentTimeMillis()+ Constants.Jwt.EXPIRATION)).signWith(SignatureAlgorithm.HS512, Constants.Jwt.KEY).compressWith(CompressionCodecs.GZIP).compact(); } //获取用户名public static String getUserName(String token) {return Jwts.parser().setSigningKey(Constants.Jwt.KEY).parseClaimsJws(token).getBody().getSubject(); }}

涉及常量
public interface Constants {public interface Jwt{/*** 密钥*/String KEY = "123123"; /*** 过期时间*/long EXPIRATION = 7200000; /*** 请求头*/String TOKEN_HEAD = "Authorization"; }}

实体类和Service
为了减少对实体类的入侵,我又定义了一个对象
原实体类
/** * 用户信息 * */@Data@TableName("tb_user_info")public class UserInfo implements Serializable { private static final long serialVersionUID = 1L; /*** 主键id*/ @TableId(type = IdType.AUTO) private Integer id; /*** 登陆账号*/ private String loginName; /*** 显示名*/ private String dispName; /*** 密码*/ private String password; /*** 状态,1正常,2禁用*/ private Byte status; /*** 创建人*/ @TableField(fill = FieldFill.INSERT) private Integer createBy; /*** 更新人*/ @TableField(fill = FieldFill.INSERT_UPDATE) private Integer updateBy; /*** 创建时间*/ @TableField(fill = FieldFill.INSERT) private Date createTime; /*** 更新时间*/ @TableField(fill = FieldFill.INSERT_UPDATE) private Date updateTime; /*** 1正常, 0 删除*/ @TableLogic private Byte deleted; }

定义对象
@Datapublic class UserSecurity implements UserDetails {private static final long serialVersionUID = -6777760550924505136L; private UserInfo userInfo; private List permissionValues; public UserSecurity(UserInfo userInfo) {this.userInfo = userInfo; } @Overridepublic Collection getAuthorities() {Collection authorities = new ArrayList<>(); for(String permissionValue : permissionValues) {if(StringUtils.isEmpty(permissionValue)) continue; SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue); authorities.add(authority); } return authorities; } @Overridepublic String getPassword() {return userInfo.getPassword(); } @Overridepublic String getUsername() {return userInfo.getLoginName(); } @Overridepublic boolean isAccountNonExpired() {return true; } @Overridepublic boolean isAccountNonLocked() {return true; } @Overridepublic boolean isCredentialsNonExpired() {return true; } @Overridepublic boolean isEnabled() {return true; }}

service
@Servicepublic class UserInfoServiceImpl implementsUserInfoService, UserDetailsService { @AutowiredUserInfoMapper userInfoMapper; @AutowiredPermissionService permissionService; @Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("login_name", username); UserInfo userInfo = userInfoMapper.selectOne(queryWrapper); List rights = permissionService.listPermissions(username); UserSecurity userSecurity = new UserSecurity(userInfo); userSecurity.setPermissionValues(rights); return userSecurity; }}

配置类
@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter { @AutowiredUserInfoServiceImpl userInfoService; @AutowiredJwtAuthorizationTokenFilter jwtAuthorizationTokenFilter; @AutowiredTokenLoginFilter tokenLoginFilter; @AutowiredJwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowiredpublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoderBean()); } @Overrideprotected void configure(HttpSecurity http) throws Exception {http.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().csrf().disable().authorizeRequests().antMatchers("/login").permitAll().antMatchers("/hello").permitAll().antMatchers("/swagger-ui.html").permitAll().antMatchers("/webjars/**").permitAll().antMatchers("/swagger-resources/**").permitAll().antMatchers("/v2/*").permitAll().antMatchers("/csrf").permitAll().antMatchers("/doc.html").permitAll().antMatchers(HttpMethod.OPTIONS, "/**").anonymous().anyRequest().authenticated().and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().addFilterAt(tokenLoginFilter, UsernamePasswordAuthenticationFilter.class).addFilterAfter(jwtAuthorizationTokenFilter, TokenLoginFilter.class).httpBasic(); } @Beanpublic PasswordEncoder passwordEncoderBean() {return new BCryptPasswordEncoder(); } @Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager(); }}

过滤器
一个负责登录
一个负责鉴权
@Componentpublic class JwtAuthorizationTokenFilter extends OncePerRequestFilter { @AutowiredPermissionService permissionService; @AutowiredUserInfoServiceImpl userDetailsService; @Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {//获取当前认证成功用户权限信息UsernamePasswordAuthenticationToken authRequest = getAuthentication(request); //判断如果有权限信息,放到权限上下文中if (authRequest != null) {SecurityContextHolder.getContext().setAuthentication(authRequest); } chain.doFilter(request, response); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {//从header获取tokenString token = request.getHeader(Constants.Jwt.TOKEN_HEAD); if (token != null) {//从token获取用户名String loginName = JwtTokenUtils.getUserName(token); //获取权限列表UserInfo userInfo = userDetailsService.selectByLoginName(loginName); List permissions = permissionService.listPermissions(loginName); Collection authority = new ArrayList<>(); for (String permissionValue : permissions) {SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue); authority.add(auth); }return new UsernamePasswordAuthenticationToken(userInfo, token, authority); }return null; }}

Componentpublic class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { public TokenLoginFilter() {this.setPostOnly(false); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST")); } @Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {//获取表单提交数据try {UserInfo user = new ObjectMapper().readValue(request.getInputStream(), UserInfo.class); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getLoginName(), user.getPassword(),null); return super.getAuthenticationManager().authenticate(authenticationToken); } catch (IOException e) {e.printStackTrace(); throw new RuntimeException(); }} @Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {UserSecurity userSecurity = (UserSecurity) authResult.getPrincipal(); String token = JwtTokenUtils.createToken(userSecurity.getUsername()); ResponseUtils.out(response, R.ok(token)); } @Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {ResponseUtils.out(response, R.fail(ServiceError.LOGIN_FAIL)); } @Autowired@Overridepublic void setAuthenticationManager(AuthenticationManager authenticationManager) {super.setAuthenticationManager(authenticationManager); }}

异常处理
登陆失败异常处理,自定义返回类型
@Componentpublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseUtils.out(response, R.fail(ServiceError.AUTHENTICATION_FAIL)); }}

至于权限和角色,得看具体的业务,我这边就不贴出来了。
流程分析 总体分析
代码肯定不是凭空出来的,必定有章可循。
首先看网上找的这个图:
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

最前面两个过滤器:密码检验、权限认证。(顺序很重要,鉴权之前总得给人家输入账号密码的机会吧)
想一下传统的session会话模式,登录成功后,服务器维护了session,浏览器得cookie里存放了sessionId,用户访问的时候浏览器自动带上sessionId。
放在以前,服务器端的工作都是是框架帮我们做了(原生session来自于tomcat)。
换成jwt后,需要前端自己去存储token,后台自己来解析token。
但是流程还是一样的,用户登录、获取token、携带token、校验token。。。。。。
登录校验过滤器分析
分析完流程后,就需要看看,如果不用jwt,框架自身是如何实现的。
找到UsernamePasswordAuthenticationFilter这个类
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

可以发现这个类主要是用来检验密码生成,凭证的。那我们只要继承这个UsernamePasswordAuthenticationFilter类,重写attemptAuthentication,就可以实现登录校验功能。
所以我们的代码是这样的:
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

权限认证过滤器分析
需要看一下BasicAuthenticationFilter的源码
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

其中的关键方法:
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

思考一下,我们如果需要用户信息,会从哪里取?
自然是请求头,前端访问的时候,会把token放在请求头。
取得token后,就要开始解析token了。token正确,将认证信息以及权限信息注入,token不正确则可以抛出异常。这就是我们第二个过滤器的由来
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

一个地方需要注意一下:
这边的userInfo,就是对应的principal
SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

比如这么用:
@Componentpublic class SecurityUtils {/*** 获取当前用户信息* @return 用户信息*/public UserInfo getCurrentUser() {return (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); }}

这里的UserInfo对象,就是principal。
如果你上面放的不对象,而是用户名或者id,那么获取principal的时候,自然是对应的用户名或者id了。
种瓜得瓜,种豆得豆。
配置文件
截取核心片段加点注释。
SpringSecurity整合jwt权限认证的全流程讲解
文章图片

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

    推荐阅读