1、jwt原理
JWT(JSON Web Token)是一种用于身份验证和授权的开放标准(RFC 7519)。它是一种轻量级的令牌格式,用于在网络应用间传输声明(claims)。JWT使用JSON对象来编码声明,并使用数字签名(或加密)以确保声明的完整性和安全性。
1.1、内容:
内容包含三个部分:头部(Header)、载荷(Payload)和签名(Signature):
- 头部(Header):
头部通常由两部分组成:令牌的类型(typ)和使用的签名算法(alg)。常见的签名算法包括 HMAC SHA256(HS256)、RSA SHA256(RS256)等。头部会通过Base64编码后放置在JWT的第一个部分。
- 载荷(Payload):
载荷包含了一组声明(claims),用于携带关于用户和其他实体的信息。有三种类型的声明:
Registered Claims(注册声明):这些是预定义的声明,包含一些标准字段,如iss(签发者)、sub(主题)、exp(过期时间)、aud(受众)等。
Public Claims(公共声明):这些是自定义的声明,可以根据应用程序的需求添加任意的字段。
Private Claims(私有声明):这些是自定义的声明,用于在双方之间共享信息,但不建议在JWT中公开传输。
载荷会被Base64编码后放置在JWT的第二个部分。
- 签名(Signature):
签名是对头部和载荷进行签名的结果,以确保JWT的完整性和真实性。签名通常使用头部中指定的算法和密钥进行计算。签名可以防止未经授权的修改和篡改JWT。签名是JWT的第三个部分。
完整的JWT由这三个部分通过点号(.)连接而成,形如:xxxxx.yyyyy.zzzzz。
2、springBoot项目使用springSecurity框架实现单点登录demo
2.1、创建一个最基本的springBoot项目(此处略),然后引入springSecurity、redis、jwt相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--jwt的依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
2.2、配置类及工具类代码实现
jwt工具类
package com.loong.nba.player.util;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* @author
* @date 2023/5/18
*/
@Component
public class JwtUtil {
private final Key secretKey;
private final long expirationTime;
@Autowired
private StringRedisTemplate redisTemplate;
public JwtUtil() {
//这里我是用随机生成的,兄弟们也可以手动写死一个
this.secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
this.expirationTime = 1200000;
}
/**
* 生成jwt令牌
*
* @param username 用户名
* @return token
*/
public String generateToken(String username) {
Date now = new Date();
Date expirationDate = new Date(System.currentTimeMillis() + expirationTime);
String token = Jwts.builder()
.setSubject(username)
.setIssuedAt(now)
.setExpiration(expirationDate)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
redisTemplate.opsForValue().set(username, token, expirationTime, TimeUnit.MILLISECONDS);
return token;
}
/**
* 解析令牌
*
* @param token token
* @return 内容
*/
public String getUsernameFormToken(String token) {
return extractClaims(token).getBody().getSubject();
}
// 验证token
public boolean validateToken(String token) {
try {
Jws<Claims> claims = extractClaims(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private Jws<Claims> extractClaims(String token) {
JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
return parser.parseClaimsJws(token);
}
}
自定义Filter类
package com.loong.nba.player.filter;
import com.loong.nba.player.util.JwtUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
/**
* @author
* @date 2023/5/18
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final String tokenHeader = "Authorization";
private final String tokenPrefix = "Bearer ";
private final JwtUtil jwtUtil;
private final StringRedisTemplate redisTemplate;
public JwtAuthenticationFilter(JwtUtil jwtUtil, StringRedisTemplate redisTemplate) {
this.jwtUtil = jwtUtil;
this.redisTemplate = redisTemplate;
}
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//从请求头拿到token
String header = request.getHeader(tokenHeader);
if (!ObjectUtils.isEmpty(header) && header.startsWith(tokenPrefix)) {
//去掉头部的字符串
String token = header.replace(tokenPrefix, "");
if (!ObjectUtils.isEmpty(token) && jwtUtil.validateToken(token)) {
//解析token,拿到usename
String username = jwtUtil.getUsernameFormToken(token);
//再从redis中取出对应jwt(不存在有可能是token有问题,还有就是redis里面token已经过期了)
if (redisTemplate.opsForValue().get(username) != null) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
username, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
chain.doFilter(request, response);
}
}
springSecruity配置类
package com.loong.nba.player.config;
import com.loong.nba.player.filter.JwtAuthenticationFilter;
import com.loong.nba.player.util.JwtUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author
* @date 2023/5/18
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//这里是允许登录接口通过,不用校验。以及后续关联我们自定义的filter类
http.csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3、业务代码
实体类
最简单的user
public class UserDO {
private String name;
private String password;
public UserDO() {
}
public UserDO(String name, String password) {
this.name = name;
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Controller
package com.loong.nba.player.controller;
import com.loong.nba.player.pojo.UserDO;
import com.loong.nba.player.util.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
/**
* @author
* @date 2023/5/15
*/
@RestController
public class LoginController {
private final JwtUtil jwtUtil;
public LoginController(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@PostMapping("/login")
public ResponseEntity<UserDO> postLogin(@RequestBody UserDO userDO, HttpServletResponse response) {
//查询持久层代码以及校验密码部分这里不写了,直接假数据定义
//正常应该是前端拿到的密文,通过后端一系列的解密加验证密码,才能通过,因为只演示框架功能,这里逻辑代码就不写了
String username = "admin";
String token = jwtUtil.generateToken(username);
response.addHeader("Authorization", "Bearer " + token);
return ResponseEntity.ok(userDO);
}
@GetMapping("/test")
public String test(){
return "hello test";
}
}
2.4 测试
代码可以直接运行,跑起来以后,请求登录接口,从response中拿到headers中Key为Authorization的值,就是token
然后再请求别的接口,如代码中的test接口,和登录接口不一样,它一定要在请求头中添加上Authorization这个键值对,不然会报403异常。