概念
JWT:JSON Web Token,一个基于JSON的令牌标准,令牌中可以含有有意义的信息
会话管理 之 Session
Session: 见 //www.greatytc.com/p/0f98d31cf1b9
会话管理 之 Token
原理:
1、用户登录时,服务器端 加密 用户ID 和 过期时间 组成的字符串,得到token,发放给客户端
2、客户端每次发送请求都带上token
3、服务器端 解密token 或者 验证token,从而得到用户ID 和 会话过期时间
token生成方案:
token = user_id|expiry_date|HMAC(user_id|expiry_date, key)
token = AES(user_id|expiry_date, key)
token = RSA(user_id|expiry_date, private key)
HMAC(Hash-based Message Authentication Code):基于哈希算法的消息认证机制,相当于有密钥参与的单向加密算法
HS256:即哈希算法为SHA-256 的 HMAC
Session 与 Token方案对比
1、单点登录系统中,采用Token方案,原始会话信息只在平台管理,应用向平台查询会话信息并单独保留,应用之间无需共享会话信息,从而跨域更容易
2、分布式系统中,采用Token方案,应用可以不存储会话信息(也就无需共享会话信息),只要验证token即可得到用户ID 和 过期时间,从而获取权限信息
Token 格式
格式:{header}.{payload}.{signature}
示例:eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJFVEgiLCJleHAiOjE1ODE5NTAzMjgsImlhdCI6MTU4MTk0MzEyOH0.as9ypxng9aeRDG2fGWNnZOLz9Mc86suO_0ZgSKI9LTvKC9w0q1vVYUTg4zqToXX34fFU3cWJz3VKLUHx4SyGzw
header 为 {"typ":"JWT","alg":"HS256"} 转 Base64,指明 类型 和 生成(验证)算法
payload 也是 JSON字符串 转 Base64,payload中的字段分为标准字段(声明claim) 和 自定义字段
payload中的标准字段:
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: jwt的起始生效时间
iat: jwt的签发时间
jti: jwt的唯一标识
signature 即 对 header + payload 进行签名得到
Spring Security 整合 JWT 应用组成
1、pom.xm
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、application.yml
jwt:
secret: secret # 用于生成token的密钥
expiration: 7200000 # token有效期
token: Authorization # http header里token 所在的字段名
3、JwtTokenUtil.java JWT工具类
@Component
public class JwtTokenUtil implements Serializable {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private Clock clock = DefaultClock.INSTANCE;
public String generateToken(String subject) {
Map<String, Object> claims = new HashMap<>();
Date createdDate = clock.now();
Date expirationDate = calculateExpirationDate(createdDate);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
private Date calculateExpirationDate(Date createdDate) {
return new Date(createdDate.getTime() + expiration);
}
public Boolean validateToken(String token, String username) {
final String tokenUsername = getUsernameFromToken(token);
return (tokenUsername.equals(username) && !isTokenExpired(token)
);
}
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
}
4、JwtAuthorizationTokenFilter.java 验证token,获取权限,将权限存入上下文
@Component
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.token}")
private String tokenHeader;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
final String requestHeader = request.getHeader(this.tokenHeader); // 从header 中获取token
String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) { // token 以 Bearer 为前缀,表示 Bearer Token ,区别于MAC Token
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken); // 从token中解析出 username
} catch (ExpiredJwtException e) {
}
}
// 验证token
if (username != null && jwtTokenUtil.validateToken(authToken, username)) {
UserDetails userDetails = this.loadUserByUsername(username); // 查询UserDetails
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication); // 在上下文中记录UserDetails
}
chain.doFilter(request, response);
}
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<GrantedAuthority> authorityList = new ArrayList<>();
/* 此处查询数据库得到角色权限列表,这里可以用Redis缓存以增加查询速度 */
authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); // 角色 需要以 ROLE_ 开头
return new org.springframework.security.core.userdetails.User(username, "", true, true,
true, true, authorityList);
}
}
5、JwtAuthenticationEntryPoint.java 处理凭证无效的情况
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "凭证无效");
}
}
6、SecurityConfig.java 配置 Spring Security
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 有这个注解, @PreAuthorize 才能有效
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAuthorizationTokenFilter authenticationTokenFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) // 添加token过滤器
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) // 凭证无效时的处理
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous() // 允许匿名访问
.anyRequest().authenticated()
.and()
.csrf().disable()
// 不使用session,此策略 使得 每次请求都要自行处理权限问题(往SecurityContextHolder.context中添加和查询Authentication)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
7、LoginController.java 实现登录接口,生成token 返回
@RestController
public class LoginController {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public String login(@RequestBody User user, HttpServletRequest request) {
/* 在这里验证用户名和密码,验证成功则生成token返回 */
return jwtTokenUtil.generateToken(user.getUsername()); // 生成 Token,返回给客户端
}
@PreAuthorize("hasAnyRole('USER')") // 对单个方法进行权限控制
@GetMapping("/me")
public String me() {
// 从上下文中获取 UserDetails
UserDetails userDetails = (UserDetails) org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return userDetails.getUsername() + "," + userDetails.getPassword(); // 可以使用重写的 UserDetails ,使得 UserDetails 里可以存放更多用户信息
}
}