SpringBoot2.3.4整合SpringSecurity+Jwt实现权限管理

概述

本文主要介绍SpringBoot整合SpringSecurity和Jwt实现权限管理的认证和授权,所用到的技术栈及版本如下:
SpringBoot:2.3.4
SpringSecurity
mybatis-plus:3.4.0
jjwt:0.9.1
hutool:5.4.5
fastjson:1.2.74
若想先了解SpringBoot整合SpringSecurity的实现过程可以参考另一篇SpringBoot2.3.4整合SpringSecurity实现权限管理

JWT简介

什么是JWT

JWT是JSON Web Token的简称,是一个开放的行业标准(RFC7519),一种JSON风格的轻量级的授权和身份认证规范,它定义了一种简洁的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证。可实现无状态、分布式的Web应用授权,更多详情请阅读JWT官网
优点

  • jwt基于json,非常方便解析
  • 可以在令牌中自定义丰富的内容,易扩展
  • 通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高
  • 资源服务使用JWT可以不依赖认证服务即可完成授权

缺点

  • JWT令牌较长,占用存储空间比较大

JWT用于前后端交互流程图如下:
JWT交互流程图JWT用户认证

JWT组成

一个完整的JWT实际上就是一个字符串,主要由三部分组成,头部、载荷、签名
头部(Header)
头部用于描述关于该JWT的基本信息,例如类型和签名所用的算法

{
	"alg": "HS512",
	"typ": "JWT"
}
  • typ:是类型
  • alg:是签名算法

负载(Payload)
用于存放有效信息,主要包括三部分

  • 标准中注册的声明(建议但不强制使用)
iss:jwt签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须要大于签发时间
nbf:定义在什么时间之前,该jwt都是不可用的
iat:jwt的签发时间
jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重复攻击
  • 公共的声明

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

  • 私有的声明

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

这个指的就是自定义的claim。比如下面那个举例中的name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

{
	"sub": "1234567890",
	"name": "John Doe",
	"iat": 1516239022
}
  • sub:标准的声明
  • name:自定义的声明(公共或私有)

签名(signature)
主要由三部分组成

  • header(base64后)
  • payload(base64后)
  • secret(密钥)

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分

什么是JJWT

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJW很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

核心代码实现

pom.xml文件中引入相关依赖包

<!--token生成与解析-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yml配置文件中添加jwt配置

token:
  header: Authorization #自定义token标识
  secret: zaqwsxcderfvbgtyhnmjuiklop #密钥
  expireTime: 3600000 #过期时间(30分钟)

header是携带JWT令牌的HTTP的Header的名称,根据实际情况自定义即可;secret是用来为JWT基础信息加解密的密钥,实际生产中密钥会更复杂和经常变更;expireTime是JWT令牌的有效时长

编写登录controller类

@RestController
public class LoginController {
    @Autowired
    private JwtAuthService jwtAuthService;
    /**
     * 登录
     * @param loginUser
     * @return
     */
    @PostMapping("/login")
    public ResultData login(@RequestBody LoginUser loginUser) {
        String token = jwtAuthService.login(loginUser.getUsername(), loginUser.getPassword());
        return ResultData.ok().data("token", token);
    }
}

编写JWT认证业务接口实现类

@Service
public class JwtAuthServiceImpl implements JwtAuthService {
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private JwtTokenUtils jwtTokenUtils;
    /**
     * 登录认证换取JWT令牌
     * @param username
     * @param password
     * @return
     */
    @Override
    public String login(String username, String password) {
        Authentication authentication = null;
        try {
            authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
            throw new RuntimeException("用户名或密码不正确!");
        }
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return jwtTokenUtils.generateToken(userDetails);
    }
}

编写JWT令牌相关工具类

@Data
@Component
public class JwtTokenUtils {

    @Value("${token.secret}")
    private String secret;
    @Value("${token.expireTime}")
    private Long expiration;
    @Value("${token.header}")
    private String header;

    /**
     * 生成token令牌
     * @param userDetails
     * @return
     */
    public String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 从claims生成令牌
     * @param claims
     * @return
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder().setClaims(claims).setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret).compact();
    }

    /**
     * 从令牌中获取用户名
     * @param token
     * @return
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 从令牌中获取数据声明
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 判断令牌是否过期
     * @param token
     * @return
     */
    public Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     * @param token
     * @return
     */
    public String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     * @param token
     * @param userDetails
     * @return
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        String username = getUsernameFromToken(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
}

编写JWT鉴权拦截器

@Component
public class JwtAuthTokenFilter extends OncePerRequestFilter {

    @Resource
    private JwtTokenUtils jwtTokenUtils;
    @Resource
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader(jwtTokenUtils.getHeader());
        if (StrUtil.isNotEmpty(token)) {
            String username = jwtTokenUtils.getUsernameFromToken(token);
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                if (jwtTokenUtils.validateToken(token, userDetails)) {
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

编写SpringSecurity配置类

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private JwtAuthTokenFilter jwtAuthTokenFilter;
    @Resource
    UserDetailsService userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //允许匿名访问
                .antMatchers("/login").anonymous()
                .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
                .antMatchers("/test")
                .hasAnyAuthority("ROLE_user", "ROLE_admin")
                .antMatchers("/system/user", "/system/role")
                .hasRole("admin")
                .anyRequest().authenticated().and()
                .csrf().disable();
        http.logout().logoutUrl("/logout");
        http.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

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

    /**
     * 强散列哈希加密实现
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

测试

本文使用postman进行相关测试
启动代码后在postman中输入localhost:8091/system/user
未登录无权限返回结果是无法访问
输入用户名和密码admin/123456,访问localhost:8091/login
登录将登录后返回的token传递到header中,再次访问localhost:8091/system/user
有权限访问
完整代码详见码云地址

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值