SpringSecurity JWT
kaico2018 人气:0Token
Token和Sessionid的思想一样。Session是存在服务器端JVM中,Token存在Redis中。
解决分布式Session数据一致性问题:Spring-Session
传统的Token,例如:用户登录成功生成对应的令牌,key:为令牌, value:userid,隐藏了数据真实性 ,同时将该token存放到redis中,返回对应的真实令牌给客户端存放。客户端每次访问后端请求的时候,会传递该token在请求中,服务器端接收到该token之后,从redis中查询如果存在的情况下,则说明在有效期内,如果在Redis中不存在的情况下,则说明过期或者token错误。
Token使用:
- 验证账号密码成功
- 生成一个令牌UUID
- 将该令牌存放到redis中,key为令牌,value值对应存放userid
- 最终返回令牌给客户端
Token验证回话信息
- 在请求头中传递该令牌
- 从Redis中验证该令牌是否有效期
- 获取value内容
- 根据userid查询用户信息,返回给客户端
Token存放数据优缺点:
缺点:
- 必须依赖服务器,占用服务器端资源
- 效率非常低
优点:
- 可以隐藏数据真实性
- 适用于分布式/微服务
- 安全性高 JWT
Jwt
JSON WEB Token JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
组成
第一部分:header (头部)
描述加密算法
HS256:属于验证签名
RSA256:属于非对称加密
第二部分:playload(载荷)
携带存放的数据 用户名称、用户头像之类,需要注意铭感数据
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
第三部分:secret (存放在服务器端)
签名值,Base64(header .playload) +秘钥
JWT和Token的区别
1、token对应的数据存放在redis中
2、JWT对应存放的数据(payload中)客户端
优缺点
优点:
1、JWT数据存放在客户端,不依赖于服务器端,减轻服务器端压力
2、效率比传统的token验证还要高
缺点
1、jwt一旦生成之后后期无法修改
2、无法销毁一个jwt
3、建议不要放敏感数据,userid、手机号
4、后端无法统计 生成JWT
手写JWT
public class Test001 { private static final String SIGN_KEY = "kaicoSignKey"; public static void main(String[] args) throws UnsupportedEncodingException { //手写jwt 封装三个部分:header、payload、sign签名 //定义header JSONObject header = new JSONObject(); header.put("alg", "HS256"); //payload JSONObject payload = new JSONObject(); payload.put("name", "kaico"); String headerEncode = Base64.getEncoder().encodeToString(header.toJSONString().getBytes()); String payloadJSONString = payload.toJSONString(); String payloadEncode = Base64.getEncoder().encodeToString(payloadJSONString.getBytes()); //sign签名值 实际上就是 md5 String sign = DigestUtils.md5DigestAsHex((payload + SIGN_KEY).getBytes()); String jwt = headerEncode + "." + payloadEncode + "." + sign; System.out.println(jwt); //解密 String payloadEncodeStr = jwt.split("\\.")[1]; String payloadDecoder = new String(Base64.getDecoder().decode(payloadEncodeStr), "UTF-8"); String newSign = DigestUtils.md5DigestAsHex((payloadDecoder + SIGN_KEY).getBytes()); System.out.println(newSign.equals(jwt.split("\\.")[2])); } }
使用工具类新建JWT
public class Test003 { private static final String SIGN_KEY = "kaicoSignKey"; public static void main(String[] args) { long now = System.currentTimeMillis(); //设置过期时间 (测试使用1秒钟) Long exp = now + 1 * 1000; JwtBuilder jwtBuilder = Jwts.builder() //payload值 .claim("userImg", "sssss") //签名值 .signWith(SignatureAlgorithm.HS256, SIGN_KEY) .setExpiration(new Date(exp)); //输出JWT的内容 String jwt = jwtBuilder.compact(); System.out.println(jwt); //解密 try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } Claims body = Jwts.parser().setSigningKey(SIGN_KEY).parseClaimsJws(jwt).getBody(); System.out.println(body.get("userImg")); } }
Springboot整合JWT
登录流程
1、验证账号密码
2、账号密码验证成功,生成JWT返回给客户端(移动app、浏览器、微信小程序)
3、客户端请求服务端,服务端验证JWT
1. base64解密jwt获取payload中的数据
2. 获取roles权限列表注册到SpringSecurity框架中
代码整合
在上次整合SpringSecurity的基础上
新增两个过滤器
package com.kaico.jwt.filter; import com.fasterxml.jackson.databind.ObjectMapper; import com.kaico.jwt.entity.UserEntity; import com.kaico.jwt.utils.JwtUtils; 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.UsernamePasswordAuthenticationFilter; 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.ArrayList; /** * @Author kaico * @Description //TODO * @Date 19:26 2022/7/25 */ public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter { /** * 获取授权管理 */ private AuthenticationManager authenticationManager; public JWTLoginFilter(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; /** * 后端登陆接口 */ super.setFilterProcessesUrl("/auth/login"); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) { try { UserEntity user = new ObjectMapper() .readValue(req.getInputStream(), UserEntity.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( user.getUsername(), user.getPassword(), new ArrayList<>()) ); } catch (IOException e) { logger.error(e.getMessage()); return null; } } @Override /** * 用户登陆成功之后验证 */ protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { UserEntity userEntity = (UserEntity) authResult.getPrincipal(); String jwtToken = JwtUtils.generateJsonWebToken(userEntity); response.addHeader("token", jwtToken); } /** * 账号或者密码错误 * @param request * @param response * @param failed * @throws IOException * @throws ServletException */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.getWriter().print("账号或者密码错误"); } }
package com.kaico.jwt.filter; import com.kaico.jwt.utils.JwtUtils; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; 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.List; /** * @Author kaico * @Description //TODO * @Date 19:37 2022/7/25 */ public class JWTValidationFilter extends BasicAuthenticationFilter { public JWTValidationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } /** * 过滤请求验证 * * @param request * @param response * @param chain * @throws IOException * @throws ServletException */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { SecurityContextHolder.getContext().setAuthentication(setAuthentication(request.getHeader("token"))); super.doFilterInternal(request, response, chain); } /** * 验证token 并且验证权限 * @param token * @return */ private UsernamePasswordAuthenticationToken setAuthentication(String token) { String username = JwtUtils.getUsername(token); if (username == null) { return null; } //解析权限列表 List<SimpleGrantedAuthority> userRoleList = JwtUtils.getUserRole(token); return new UsernamePasswordAuthenticationToken(username, null, userRoleList); } }
JWT工具类
package com.kaico.jwt.utils; import com.alibaba.fastjson.JSONArray; import com.kaico.jwt.entity.UserEntity; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; public class JwtUtils { public static final String TOKEN_HEADER = "token"; public static final String TOKEN_PREFIX = "Bearer "; private static final String SUBJECT = "kaico"; //JWT有效期 private static final long EXPIRITION = 1000 * 24 * 60 * 60 * 7; private static final String APPSECRET_KEY = "kaico_secret"; private static final String ROLE_CLAIMS = "roles"; public static String generateJsonWebToken(UserEntity user) { String token = Jwts .builder() .setSubject(SUBJECT) .claim(ROLE_CLAIMS, user.getAuthorities()) .claim("username", user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact(); return token; } /** * 生成token * * @param username * @param role * @return */ public static String createToken(String username, String role) { Map<String, Object> map = new HashMap<>(); map.put(ROLE_CLAIMS, role); String token = Jwts .builder() .setSubject(username) .setClaims(map) .claim("username", username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION)) .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact(); return token; } public static Claims checkJWT(String token) { try { final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 获取用户名 * * @param token * @return */ public static String getUsername(String token) { Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.get("username").toString(); } /** * 获取用户角色 * * @param token * @return */ public static List<SimpleGrantedAuthority> getUserRole(String token) { Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); List roles = (List) claims.get(ROLE_CLAIMS); String json = JSONArray.toJSONString(roles); List<SimpleGrantedAuthority> grantedAuthorityList = JSONArray.parseArray(json, SimpleGrantedAuthority.class); return grantedAuthorityList; } /** * 是否过期 * * @param token * @return */ public static boolean isExpiration(String token) { Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody(); return claims.getExpiration().before(new Date()); } }
修改SecurityConfig
配置类
@Override protected void configure(HttpSecurity http) throws Exception { List<PermissionEntity> allPermission = permissionMapper.findAllPermission(); ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.authorizeRequests(); allPermission.forEach((permission) -> { expressionInterceptUrlRegistry.antMatchers(permission.getUrl()). hasAnyAuthority(permission.getPermTag()); }); // 配置前后令牌登陆 expressionInterceptUrlRegistry.antMatchers("/auth/login").permitAll() .antMatchers("/**").fullyAuthenticated() .and() //配置过滤器 .addFilter(new JWTValidationFilter(authenticationManager())) .addFilter(new JWTLoginFilter(authenticationManager())).csrf().disable() //提出session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }
测试:
1、post方式请求登录接口:localhost:8080/auth/login
请求参数json,返回请求头中带有token
{
"username":"kaico_add",
"password":"kaico"
}
2、再次请求其他接口时,请求头上带上token,
存在的问题
Jwt如何实现注销?
- 客户端清除缓存,比如:浏览器cookie清除(但是服务器还是存在)
- 权限发生变化的情况下,管理员同志用户重新登录或者提示权限不足,请联系管理员开放权限。
- 建议将时间设置稍微短一点
- 使用黑名单过滤,后期对服务器端压力大
JWT是否安全?
安全机制肯定有
就算黑客篡改payload中的权限列表,必须先获取到服务器端秘钥,自己生产JWT才行。
JWT中存放userid
可以单独对userid做对称加密之后再存在payload中,解密的秘钥在服务器端,也是安全的。
加载全部内容