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

作者:小楼夜听雨QAQ 时间:2022-02-20 09:58:36 

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是否正确,通过设定过期时间来确定其有效性,不可以手动注销。

先说怎么做,再说为什么。

代码

依赖


       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-security</artifactId>
           <security.version>2.3.3.RELEASE</security.version>
       </dependency>

<dependency>
           <groupId>io.jsonwebtoken</groupId>
           <artifactId>jjwt</artifactId>
           <jwt.version>0.9.1</jwt.version>
       </dependency>

jwt工具类


public class JwtTokenUtils implements Serializable {
   private static final long serialVersionUID = -3369436201465044824L;
   //生成token
   public 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;
}

定义对象


@Data
public class UserSecurity implements UserDetails {
   private static final long serialVersionUID = -6777760550924505136L;
   private UserInfo userInfo;
   private List<String> permissionValues;
   public UserSecurity(UserInfo userInfo) {
       this.userInfo = userInfo;
   }

@Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
       Collection<GrantedAuthority> authorities = new ArrayList<>();
       for(String permissionValue : permissionValues) {
           if(StringUtils.isEmpty(permissionValue)) continue;
           SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
           authorities.add(authority);
       }

return authorities;
   }

@Override
   public String getPassword() {
       return userInfo.getPassword();
   }

@Override
   public String getUsername() {
       return userInfo.getLoginName();
   }

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

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

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

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

service


@Service
public class UserInfoServiceImpl implements  UserInfoService, UserDetailsService {

@Autowired
   UserInfoMapper userInfoMapper;

@Autowired
   PermissionService permissionService;

@Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
       QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
       queryWrapper.eq("login_name", username);
       UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);
       List<String> rights = permissionService.listPermissions(username);
       UserSecurity userSecurity = new UserSecurity(userInfo);
       userSecurity.setPermissionValues(rights);
       return userSecurity;
   }
}

配置类


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

@Autowired
   UserInfoServiceImpl userInfoService;

@Autowired
   JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter;

@Autowired
   TokenLoginFilter tokenLoginFilter;

@Autowired
   JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Autowired
   public void configure(AuthenticationManagerBuilder auth) throws Exception {
       auth.userDetailsService(userInfoService).passwordEncoder(passwordEncoderBean());
   }

@Override
   protected 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()
               ;
   }

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

@Bean
   @Override
   protected AuthenticationManager authenticationManager() throws Exception {
       return super.authenticationManager();
   }
}

过滤器

一个负责登录

一个负责鉴权


@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {

@Autowired
   PermissionService permissionService;

@Autowired
   UserInfoServiceImpl userDetailsService;

@Override
   protected 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获取token
       String token = request.getHeader(Constants.Jwt.TOKEN_HEAD);
       if (token != null) {
           //从token获取用户名
           String loginName = JwtTokenUtils.getUserName(token);
           //获取权限列表
           UserInfo userInfo = userDetailsService.selectByLoginName(loginName);
           List<String> permissions = permissionService.listPermissions(loginName);
           Collection<GrantedAuthority> authority = new ArrayList<>();
           for (String permissionValue : permissions) {
               SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
               authority.add(auth);
           }
           return new UsernamePasswordAuthenticationToken(userInfo, token, authority);
       }
       return null;
   }
}

Component
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

public TokenLoginFilter() {
       this.setPostOnly(false);
       this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
   }

@Override
   public 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();
       }
   }

@Override
   protected 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));
   }

@Override
   protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
       ResponseUtils.out(response, R.fail(ServiceError.LOGIN_FAIL));
   }

@Autowired
   @Override
   public void setAuthenticationManager(AuthenticationManager authenticationManager) {
       super.setAuthenticationManager(authenticationManager);
   }
}

异常处理

登陆失败异常处理,自定义返回类型


@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
   @Override
   public 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权限认证的全流程讲解

比如这么用:


@Component
public class SecurityUtils {
   /**
    * 获取当前用户信息
    * @return 用户信息
    */
   public UserInfo getCurrentUser() {
       return (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
   }
}

这里的UserInfo对象,就是principal。

如果你上面放的不对象,而是用户名或者id,那么获取principal的时候,自然是对应的用户名或者id了。

种瓜得瓜,种豆得豆。

配置文件

截取核心片段加点注释。

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

来源:https://blog.csdn.net/qq_37855749/article/details/111300906

标签:SpringSecurity,jwt,权限,认证
0
投稿

猜你喜欢

  • SpringBoot中利用AOP和拦截器实现自定义注解

    2022-09-14 00:26:53
  • Mybatis延迟加载和缓存深入讲解

    2022-06-02 15:50:43
  • Java数据结构之复杂度篇

    2022-06-18 02:32:09
  • Android实现多线程下载图片的方法

    2021-10-16 08:37:43
  • Java C++ 算法题解leetcode652寻找重复子树

    2022-08-17 23:58:09
  • 一篇文章弄懂Spring MVC的参数绑定

    2023-09-17 01:01:21
  • Android中执行java命令的方法及java代码执行并解析shell命令

    2022-08-27 15:45:13
  • opencv 做人脸识别 opencv 人脸匹配分析

    2023-07-09 06:34:44
  • java9开始——接口中可以定义private私有方法

    2023-03-27 06:53:15
  • 提高C# StringBuilder操作性能优化的方法

    2023-04-26 01:16:13
  • C#判断多个文本框是否为空的方法

    2022-05-12 23:05:32
  • Unity实现滑动更换界面效果

    2021-10-06 19:56:40
  • 使用javaMail实现发送邮件

    2023-01-20 21:22:26
  • SpringMVC文件上传中要解决的问题大汇总

    2023-09-02 06:09:20
  • 使用AOP的@Around后无返回值的解决

    2023-11-24 13:04:09
  • SpringBoot路径映射实现过程图解

    2023-11-13 04:01:11
  • 使用ehcache三步搞定springboot缓存的方法示例

    2021-06-25 04:44:12
  • C#中的数组用法详解

    2021-08-19 14:50:17
  • Java实战之飞翔的小鸟小游戏

    2022-10-04 20:37:20
  • Java中List.of()和Arrays.asList()的区别及原因分析

    2023-08-06 00:33:16
  • asp之家 软件编程 m.aspxhome.com