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

 更新时间:2021年06月23日 12:09:49   作者:小楼夜听雨QAQ  
这篇文章主要介绍了SpringSecurity整合jwt权限认证的全流程讲解,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

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));
    }
}

至于权限和角色,得看具体的业务,我这边就不贴出来了。

流程分析

总体分析

代码肯定不是凭空出来的,必定有章可循。

首先看网上找的这个图:

最前面两个过滤器:密码检验、权限认证。(顺序很重要,鉴权之前总得给人家输入账号密码的机会吧)

想一下传统的session会话模式,登录成功后,服务器维护了session,浏览器得cookie里存放了sessionId,用户访问的时候浏览器自动带上sessionId。

放在以前,服务器端的工作都是是框架帮我们做了(原生session来自于tomcat)。

换成jwt后,需要前端自己去存储token,后台自己来解析token。

但是流程还是一样的,用户登录、获取token、携带token、校验token。。。。。。

登录校验过滤器分析

分析完流程后,就需要看看,如果不用jwt,框架自身是如何实现的。

找到UsernamePasswordAuthenticationFilter这个类

可以发现这个类主要是用来检验密码生成,凭证的。那我们只要继承这个UsernamePasswordAuthenticationFilter类,重写attemptAuthentication,就可以实现登录校验功能。

所以我们的代码是这样的:

权限认证过滤器分析

需要看一下BasicAuthenticationFilter的源码

其中的关键方法:

思考一下,我们如果需要用户信息,会从哪里取?

自然是请求头,前端访问的时候,会把token放在请求头。

取得token后,就要开始解析token了。token正确,将认证信息以及权限信息注入,token不正确则可以抛出异常。这就是我们第二个过滤器的由来

一个地方需要注意一下:

这边的userInfo,就是对应的principal

SecurityContextHolder.getContext().getAuthentication().getPrincipal();

比如这么用:

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

这里的UserInfo对象,就是principal。

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

种瓜得瓜,种豆得豆。

配置文件

截取核心片段加点注释。

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

相关文章

  • 使用SpringBoot配置多数据源的经验分享

    使用SpringBoot配置多数据源的经验分享

    这篇文章主要介绍了使用SpringBoot配置多数据源的经验分享,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • SpringBoot如何动态改变日志级别

    SpringBoot如何动态改变日志级别

    这篇文章主要介绍了SpringBoot如何动态改变日志级别,帮助大家更好的理解和使用springboot框架,感兴趣的朋友可以了解下
    2020-12-12
  • MyBatis深入解读懒加载的实现

    MyBatis深入解读懒加载的实现

    顾名思义,懒加载就是因为偷懒了,懒得加载了,只有使用的时候才进行加载。其实,懒加载也加延迟加载,主要以应用与Mybatis的关联查询,按照设置的延迟规则,推迟对延迟对关联对象的select查询
    2022-04-04
  • MyBatis中#{}占位符与${}拼接符的用法说明

    MyBatis中#{}占位符与${}拼接符的用法说明

    这篇文章主要介绍了MyBatis中#{}占位符与${}拼接符的用法说明,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • springboot如何接收复杂参数(同时接收JSON与文件)

    springboot如何接收复杂参数(同时接收JSON与文件)

    文章介绍了在Spring Boot中同时处理JSON和文件上传时使用`@RequestPart`注解的方法,`@RequestPart`可以接收多种格式的参数,包括JSON和文件,并且可以作为`multipart/form-data`格式中的key
    2025-02-02
  • Struts2开发 基本配置与类型转换

    Struts2开发 基本配置与类型转换

    本篇文章,小编将为大家介绍关于Struts2开发 基本配置与类型转换,有需要的朋友可以参考一下
    2013-04-04
  • Java spring的三种注入方式详解流程

    Java spring的三种注入方式详解流程

    Spring框架由Rod Johnson开发,2004年发布了Spring框架的第一版。Spring是一个从实际开发中抽取出来的框架,因此它完成了大量开发中的通用步骤,留给开发者的仅仅是与特定应用相关的部分,从而大大提高了企业应用的开发效率
    2021-10-10
  • spring batch线上异常定位记录

    spring batch线上异常定位记录

    这篇文章主要为大家介绍了spring batch线上异常定位记录及异常解决,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-03-03
  • javax NotBlank和Email注解失效的解决

    javax NotBlank和Email注解失效的解决

    这篇文章主要介绍了javax NotBlank和Email注解失效的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08
  • MyBatis-Plus 修改和添加自动填充时间方式

    MyBatis-Plus 修改和添加自动填充时间方式

    这篇文章主要介绍了MyBatis-Plus 修改和添加自动填充时间方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-08-08

最新评论