一、概念
JWT:JSON Web Token,一个基于JSON的令牌标准,令牌中可以含有有意义的信息
二、会话管理 之 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 JAR包依赖
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、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(new JwtLoginFilter(authenticationManager()),
UsernamePasswordAuthenticationFilter.class)//自定义过滤器,处理app端登录返回token,使用token处理权限,web端使用session处理权限问题
.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);
}
}
4、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.app端登录,生成token
package com.qijubian.web.infrastructures.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qijubian.web.domains.models.LonginUser;
import com.qijubian.web.infrastructures.commons.JwtTokenUtil;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName JwtLoginFilter
* @Description //TODO
* @Author tangyinjian
* @Date 2020/11/10 8:50
* @Version 1.0
**/
@Component
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
private static Logger logger = LoggerFactory.getLogger(JwtLoginFilter.class);
@Autowired
private JwtTokenUtil jwtTokenUtil;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher("/doLogin", "POST"));
setAuthenticationManager(authenticationManager);
}
@Override
public Authentication attemptAuthentication(
HttpServletRequest request,
HttpServletResponse response
) throws AuthenticationException{
String username = request.getParameter("username");
String password = request.getParameter("password");
//规定时间内登录次数限制
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,password);
logger.info("用户(登录名):{} 正在进行登录验证。。。密码:{}", username);
//提交给自定义的provider组件进行身份验证和授权
Authentication authentication = getAuthenticationManager().authenticate(authRequest);
return authentication;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
// StringBuffer sb = new StringBuffer();
// for (GrantedAuthority authority : authorities) {
// sb.append(authority.getAuthority()).append(",");
// }
LonginUser user = (LonginUser) authResult.getPrincipal();
String jwt = jwtTokenUtil.generateToken(user.getUsername()); // 生成 Token,返回给客户端
// String jwt = Jwts.builder()
// .claim("authorities", userName)
// .setSubject(authResult.getName())
// .setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))//设置过期时间
// .signWith(SignatureAlgorithm.HS512, "root@123")//设置加密方式,以及key
// .compact();
//设置登录成功后返回的信息
Map<String,String> map = new HashMap<>();
user.setPassword(null);
user.setSecondaryPassword(null);
user.setAuthorities(null);
map.put("token",jwt);
map.put("userInfo",new ObjectMapper().writeValueAsString(user));
map.put("success","true");
map.put("msg","登录成功");
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
Map<String,String> map = new HashMap<>();
map.put("success","false");
map.put("msg","登录失败,"+failed.getMessage());
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(new ObjectMapper().writeValueAsString(map));
writer.flush();
writer.close();
}
}
5、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);
}
}
6、JwtTokenUtil.java JWT工具类
@Component
public class JwtTokenUtil implements Serializable {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Value("${jwt.token}")
private String tokenHeader;
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);
}
}