JWT认证原理,并整合SpringBoot
1、JWT是什么?
JWT是Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
2、JWT的结构
JWT由三部分组成,结构是:
header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
中间由.
连接起来
-
header(头部):
{ "alg": "HS256", #签名使用的算法,例如HMAC SHA256或RSA "typ": "JWT" #令牌的类型,即"JWT" }
然后在利用base64加密:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
-
payload(有效负载):
{ "uid": "1", "username": "admin", "role": "admin" }
通常是跟用户有关的实体,比如用户名,用户编号,用户角色。注意:不要在payload中携带敏感信息,比如用户密码。
然后在利用base64加密:
eyJ1aWQiOiIxIiwidXNlcm5hbWUiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9
-
signature(签名):
签名信息跟三个部分有关:header(base64),payload(base64),secret(密钥)
signature就是将header(base64)和payload(base64)使用
.
连接后的字符串,然后利用header中声明的加密算法加盐(密钥)secret进行组合加密,最后就成了signature。// java String encodeHeader = new BASE64Encoder().encode(header); String encodePayload = new BASE64Encoder().encode(payload); String encodeHeaderAndPayload = encodeHeader + "." + encodePayload; String signature = HMACSHA256(encodeHeaderAndPayload , "secret");
需要注意的是,secret密钥是保存在服务器的,是用来签发jwt和验证jwt的,同时jwt的签发也在服务端。
签名的目的:最后一部分签名,实际上是防止header和payload被人篡改,虽然,这三样东西在http传输中是暴露的,所有人都知道,但是签名的验证以及签发就只能在服务端实现,即使你修改了header或者payload,但是生成的第三部分的所需要的secret密钥别人是不知道的,所以其他人是无法篡改或者伪造的。
3、利用spring进行编码和解码
-
引入jwt依赖
<dependency><!--java-jwt--> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency>
-
测试生成token
/* * 生成header.payload.signature格式的jwt * */ @Test void encode(){ Calendar instance = Calendar.getInstance();// 获取当前时间 instance.add(Calendar.SECOND,600);// 在当前时间上增加600秒 /* * withHeader声明header * withClaim声明payload(可以声明多个) * sign声明signature(利用算法Algorithm.HMAC256(密钥)) * */ String token = JWT.create() //.withHeader() //默认是{"typ":"JWT","alg":"HS256"} .withClaim("userId", 11) .withClaim("userName", "admin") //.withExpiresAt(instance.getTime()) //token过期时间(可选) .sign(Algorithm.HMAC256("!213213%^&")); System.out.println("生成的token->"+token); }
利用JWT的实例方法create()然后再添加header、payload、signature参数。就会生成一串JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwidXNlcklkIjoxMX0.4km92AcjGRYvtwJeT8B2fhD8Qjfqq0SPvyH6m3X87cE
-
读取token中的payload
/* * 解jwt * */ @Test void decode(){ /* * 1、结合算法生成JWT验证对象 * 2、利用JWT验证对象验证token的签名是否正确 * 3、再从验证通过后的decodedJWT对象中获取参数 * */ JWTVerifier verifier = JWT.require(Algorithm.HMAC256("!213213%^&")).build(); DecodedJWT decodedJWT = verifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImFkbWluIiwidXNlcklkIjoxMX0.4km92AcjGRYvtwJeT8B2fhD8Qjfqq0SPvyH6m3X87cE"); System.out.println(decodedJWT.getHeader()); // header的base64编码 System.out.println(decodedJWT.getPayload()); // payload的base64编码 System.out.println(decodedJWT.getSignature()); // signature的base64编码 System.out.println(decodedJWT.getExpiresAt()); // 获得jwt的失效时间 System.out.println(decodedJWT.getClaim("userId").asInt()); // payload中的userId的值 System.out.println(decodedJWT.getClaim("userName").asString()); // payload中的userName的值 }
注意必须先结合算法验证token中的签名,然后才能获取token中的参数。
4、利用java-jwt验证token时的常见异常
SignatureVerificationException 签名不一致
TokenExpiredException 令牌时效过期
AlgorithmMismatchException 算法不匹配
InvalidClaimException payload失效
5、SpringBoot-web集成JWT
因为我们需要从数据库中查询是否存在该用户,然后再给予用户授权信息,所以需要持久化的数据层。
-
封装好JWTUtils
负责生成token和验证token就可以了
JWTUtils.java:
public class JWTUtils { /* * secret密钥 * */ private static final String SECRET = "!d$!3213#@F3@^G"; /** * 生成JWT对象 * @param payloadMap 有效负载集合 * @return token JWT令牌 */ public static String createToken(Map<String,String> payloadMap){ //1、创建JWT的builder构造器 JWTCreator.Builder jwtBuilder = JWT.create(); //2、遍历payloadMap然后利用withClaim添加到JWT中 payloadMap.forEach((key,value)->{ //System.out.println(key+"-->"+value); jwtBuilder.withClaim(key,value); }); Calendar instance = Calendar.getInstance(); instance.add(Calendar.DATE,7); // 7天token有效期 String token = jwtBuilder .withExpiresAt(instance.getTime()) //3、添加JWT失效时间 .sign(Algorithm.HMAC256(SECRET)); //4、利用算法结合SECRET给JWT签名 return token; } /** * 获得验证签名后的JWT对象: * @param token JWT令牌 * @return decodedJWT 解密后的JWT对象 */ public static DecodedJWT getTokenInfo(String token){ //1、结合算法验证JWT的签名 JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build(); //2、解密JWT对象 DecodedJWT decodedJWT = verifier.verify(token); return decodedJWT; }
-
引入相关依赖
<dependency><!--java-jwt--> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.10.3</version> </dependency> <dependency><!--springboot-web--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency><!--mybatis--> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.4</version> </dependency> <dependency><!--lombok--> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency><!--mysql--> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.38</version> </dependency> <dependency><!--springboot-test--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
-
实体层pojo
User.java:
@Data @NoArgsConstructor @AllArgsConstructor @Component public class User { /** * 用户编号 */ private Integer uid; /** * 用户名 */ private String username; /** * 用户密码 */ private String password; }
-
mapper对数据库中数据进行增删改查
这是对user表进行增删改查,根据用户名和密码进行查询(用户登录验证用),查询所有用户是登录成功后能够进行的业务
UserMapper.java:
@Mapper @Repository public interface UserMapper { /** * 根据用户名查询用户(用于登录) * @param username 用户名 * @param password 密码 * @return 查询到的用户 */ User selectUserByUsernameAndPassword(@Param("username")String username, @Param("password")String password); /** * 查询所有用户 * @return 用户集合 */ List<User> selectAllUser(); }
UserMapper.xml:
<mapper namespace="cn.wqk.demo.mapper.UserMapper"> <select id="selectUserByUsernameAndPassword" resultType="cn.wqk.demo.pojo.User" parameterType="string"> SELECT * FROM user WHERE username=#{username} AND password=#{password} </select> <select id="selectAllUser" resultType="cn.wqk.demo.pojo.User"> SELECT * FROM user </select> </mapper>
-
service进一步封装查询出来的数据
登录业务
LoginService.java:
@Service public interface LoginService { /** * 检查登录是否成功 * @param username 用户名 * @param password 用户密码 * @return 查询成功的用户的完整信息 */ User checkLogin(String username,String password) throws RuntimeException; }
LoginServiceImpl.java:
如果用户名和密码查询成功,则直接返回查询到的用户对象,否则抛出异常。
@Service public class LoginServiceImpl implements LoginService { @Autowired UserMapper userMapper; @Override public User checkLogin(String username, String password) throws RuntimeException { User userDB = userMapper.selectUserByUsernameAndPassword(username,password); if (userDB!=null){ // 用户存在,返回查询到的用户信息 return userDB; } throw new RuntimeException("认证失败,请重新登录!"); //用户不存在,抛出异常 } }
UserService.java:
@Service public interface UserService { /** * 查询所有用户 * @return 用户集合 */ List<User> allUser(); }
UserServiceImpl.java:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public List<User> allUser() { return userMapper.selectAllUser(); } }
-
controller实现接口进行登录和访问
登录的controller:
@PostMapping("/login") public Map<String,Object> login(String username,String password){ System.out.println(username); System.out.println(password); // 初始化返回给前端的携带状态码的map Map<String, Object> returnMap = new HashMap<>(); try { // 认证成功 User userDB = loginService.checkLogin(username, password); Map<String, String> payloadMap = new HashMap<>(); payloadMap.put("uid",userDB.getUid().toString()); payloadMap.put("username",userDB.getUsername()); String token = JWTUtils.createToken(payloadMap); // 生成token returnMap.put("state",true); returnMap.put("msg","认证成功"); returnMap.put("token",token); } catch (RuntimeException e) { // 认证失败 returnMap.put("state",false); returnMap.put("msg",e.getMessage()); } return returnMap; }
逻辑就是获取到前端传来的username和password,然后用于LoginService进行验证,如果验证通过则再将User对象的非敏感信息塞进token理并且生成token,然后返回给前端。
查看所有用户业务的controller:
@PostMapping("/allUser") public Map<String,Object> allUser(String token){ System.out.println(token); HashMap<String, Object> returnMap = new HashMap<>(); returnMap.put("state",false); try { //验证token DecodedJWT decodedJWT = JWTUtils.getTokenInfo(token); returnMap.put("state",true); returnMap.put("msg","认证成功"); List<User> userList = userService.allUser(); returnMap.put("data",userList); } catch (SignatureVerificationException e){ //签名不一致 returnMap.put("msg","签名不一致"); } catch (TokenExpiredException e){ //token过期 returnMap.put("msg","token过期"); } catch (AlgorithmMismatchException e) { // 算法不匹配 returnMap.put("msg","签名算法不匹配"); } catch (InvalidClaimException e) { // payload失效 returnMap.put("msg","payload已失效"); } return returnMap; }
逻辑就是从前端里获取传来的token,然后第一步就是验证token,如果验证成功则将查询到的所有用户塞进返回的Map对象里,如果验证不成功,则返回响应的异常。
所以SpringBoot-web集成JWT的完整思路就是:登录生成token,业务验证token
先从前端获取用户名和密码进行登录,如果登录成功则生成token并且塞进用户的request里面,这样用户后面所有的请求都必须携带这个token。用户登录成功想要进行业务的话,就需要先验证token是否有效并且合法,如果合法就能进行相应的业务,否则返回异常。
6、配置Web拦截器来拦截器并且结合JWT来验证
上述的方法已经能够进行发放token,并且验证token是否合法了,但是有一个问题就是假如每有一个请求我都需要验证的话我就需要写很多重复的代码,所以我就可以交给SpringWeb的拦截器来做。
JWTInterceptor.java:
public class JWTInterceptor implements HandlerInterceptor {
/**
* 拦截器验证token,验证成功放行,否则不放行并且在response里塞入状态和错误信息
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token"); // 从request请求头中获取token
HashMap<String, Object> returnMap = new HashMap<>(); //初始化返回给前端的map
try {
JWTUtils.getTokenInfo(token); //token验证
return true; //token验证通过过滤器放行
} catch (SignatureVerificationException e) { //签名不一致
returnMap.put("msg","签名不一致");
} catch (TokenExpiredException e) { //token已过期
returnMap.put("msg","token已过期");
} catch (AlgorithmMismatchException e) { //算法不匹配
returnMap.put("msg","算法不匹配");
} catch (InvalidClaimException e) { // payload已失效
returnMap.put("msg","payload已失效");
}
returnMap.put("status",false);
String json = new ObjectMapper().writeValueAsString(returnMap); //利用jackson将map转为json
response.setContentType("application/json;charset=utf-8"); //设置response响应格式
response.getWriter().println(json); //将json塞进response里
return false;
}
}
从request请求中获取键为token的header,然后进行验证,如果验证通过直接放行,否则返回响应的状态和错误信息。需要注意的是interceptor的返回值是true放行,false不放行,所以我们需要在不放行的response返回信息里面塞进相应的提示信息,所以就需要先将map集合转为json对象,然后写入到response里记得设置response的格式。
写好interceptor拦截器后就需要注册拦截器:
InterceptorConfig.java:
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor()) // 注册JWT拦截器
.addPathPatterns("/user/**") // 要拦截的请求
.excludePathPatterns("/login/**"); // 不拦截的请求
}
}
7、关于JWT需要注意的
由于JWT生成的token的特性,token是存在客户端的,验证和发放是在服务端的,所以,并且在服务端的上验证token的环节只是利用算法和密钥来验证token而已。所以即使服务器重新启动,依旧不影响token的验证,仍然能验证通过。所以,建议,如果在修改了服务器的相关配置后,建议修改密钥,这样就可以使得客户端重新生成一次token,否则很有可能用户可以拿之前的token进行现在的业务。
由于配置了拦截器,如果需要在业务中获取token中的payload的话就直接从request的header中获取就可以了。