概述
本文主要介绍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对象,传递的信息经过数字签名可以被验证。
- jwt基于json,非常方便解析
- 可以在令牌中自定义丰富的内容,易扩展
- 通过非对称加密算法及数字签名技术,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