⽬录
概述⼯作流程
登录阶段认证阶段关于有效期对⽐Session整合Springboot
导⼊java-jwt包⼯具类的编写注解类的编写拦截器的编写接⼝的编写
JSON Web Token是⽬前最流⾏的跨域认证解决⽅案,,适合前后端分离项⽬通过Restful API进⾏数据交互时进⾏⾝份认证
关于Shiro整合JWT,可以看这⾥:
概述
由于概念性内容⽹上多的是,所以就不详细介绍了具体可以看这⾥:我总结⼏个重点:
JWT,全称Json Web Token,是⼀种令牌认证的⽅式长相:
头部:放有签名算法和令牌类型(这个就是JWT)
载荷:你在令牌上附带的信息:⽐如⽤户的id,⽤户的电话号码,这样以后验证了令牌之后就可以直接从这⾥获取信息⽽不⽤再查数据库了签名:⽤来加令牌的
安全性:由于载荷⾥的内容都是⽤BASE64处理的,所以是没有保密性的(因为BASE64是对称的),但是由于签名认证的原因,其他⼈很难伪造数据。不过这也意味着,你不能把敏感信息⽐如密码放⼊载荷中,毕竟这种可以被别⼈直接看到的,但是像⽤户id这种就⽆所谓了
⼯作流程
登录阶段
⽤户⾸次登录,通过账号密码⽐对,判定是否登录成功,如果登录成功的话,就⽣成⼀个jwt字符串,然后放⼊⼀些附带信息,返回给客户端。
这个jwt字符串⾥包含了有⽤户的相关信息,⽐如这个⽤户是谁,他的id是多少,这个令牌的有效时间是多久等等。下次⽤户登录的时候,必须把这个令牌也⼀起带上。
认证阶段
这⾥需要和前端统⼀约定好,在发起请求的时候,会把上次的token放在请求头⾥的某个位置⼀起发送过来,后端接受到请求之后,会解析jwt,验证jwt是否合法,有没有被伪造,是否过期,到这⾥,验证过程就完成了。
不过服务器同样可以从验证后的jwt⾥获取⽤户的相关信息,从⽽减少对数据库的查询。⽐如我们有这样⼀个业务:“通过⽤户电话号码查询⽤户余额”
如果我们在jwt的载荷⾥事先就放有电话号码这个属性,那么我们就可以避免先去数据库根据⽤户id查询⽤户电话号码,⽽直接拿到电话号码,然后执⾏接下⾥的业务逻辑。
关于有效期
由于jwt是直接给⽤户的,只要能验证成功的jwt都可以被视作登录成功,所以,如果不给jwt设置⼀个过期时间的话,⽤户只要存着这个jwt,就相当于永远登录了,⽽这是不安全的,因为如果这个令牌泄露了,那么服务器是没有任何办法阻⽌该令牌的持有者访问的(因为拿到这个令牌就等于随便冒充你⾝份访问了),所以往往jwt都会有⼀个有效期,通常存在于载荷部分,下⾯是⼀段⽣成jwt的java代码:
return JWT.create().withAudience(userId) .withIssuedAt(new Date()) <---- 发⾏时间 .withExpiresAt(expiresDate) <---- 有效期 .withClaim(\"sessionId\ .withClaim(\"userName\ .withClaim(\"realName\
.sign(Algorithm.HMAC256(userId+\"HelloLehr\"));
在实际的开发中,令牌的有效期往往是越短越安全,因为令牌会频繁变化,即使有某个令牌被别⼈盗⽤,也会很快失效。但是有效期短也会导致⽤户体验不好(总是需要重新登录),所以这时候就会出现另外⼀种令牌—refresh token刷新令牌。刷新令牌的有效期会很长,只要刷新令牌没有过期,就可以再申请另外⼀个jwt⽽⽆需登录(且这个过程是在⽤户访问某个接⼝时⾃动完成的,⽤户不会感觉到令牌替换),对于刷新令牌的具体实现这⾥就不详细
讲啦(其实因为我也没深⼊研究过XD…)
对⽐Session
在传统的session会话机制中,服务器识别⽤户是通过⽤户⾸次访问服务器的时候,给⽤户⼀个sessionId,然后把⽤户对应的会话记录放在服务器这⾥,以后每次通过sessionId来找到对应的会话记录。这样虽然所有的数据都存在服务器上是安全的,但是对于分布式的应⽤来说,就需要考虑session共享的问题了,不然同⼀个⽤户的sessionId的请求被⾃动分配到另外⼀个服务器上就等于失效了
⽽Jwt不但可以⽤于登录认证,也把相应的数据返回给了⽤户(就是载荷⾥的内容),通过签名来保证数据的真实性,该应⽤的各个服务器上都有统⼀的验证⽅法,只要能通过验证,就说明你的令牌是可信的,我就可以从你的令牌上获取你的信息,知道你是谁了,从⽽减轻了服务器的压⼒,⽽且也对分布式应⽤更为友好。(毕竟就不⽤担⼼服务器session的分布式存储问题了)
整合Springboot
导⼊java-jwt包
导⼊java-jwt包:
这个包⾥实现了⼀系列jwt操作的api(包括上⾯讲到的怎么校验,怎么⽣成jwt等等)如果你是Maven玩家:pom.xml⾥写⼊
如果你是Gradle玩家:build.gradle⾥写⼊
compile group: 'com.auth0', name: 'java-jwt', version: '3.8.3'
如果你是其他玩家:maven中央仓库地址点
⼯具类的编写
代码如下:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;import java.io.Serializable;import java.util.Calendar;import java.util.Date;/**
* @author Lehr
* @create: 2020-02-04 */
public class JwtUtils {
/**
签发对象:这个⽤户的id 签发时间:现在 有效时间:30分钟
载荷内容:暂时设计为:这个⼈的名字,这个⼈的昵称 加密密钥:这个⼈的id加上⼀串字符串 */
public static String createToken(String userId,String realName, String userName) { Calendar nowTime = Calendar.getInstance(); nowTime.add(Calendar.MINUTE,30); Date expiresDate = nowTime.getTime();
return JWT.create().withAudience(userId) //签发对象 .withIssuedAt(new Date()) //发⾏时间 .withExpiresAt(expiresDate) //有效时间
.withClaim(\"userName\载荷,随便写⼏个都可以 .withClaim(\"realName\
.sign(Algorithm.HMAC256(userId+\"HelloLehr\")); //加密 }
/**
* 检验合法性,其中secret参数就应该传⼊的是⽤户的id * @param token
* @throws TokenUnavailable */
public static void verifyToken(String token, String secret) throws TokenUnavailable { DecodedJWT jwt = null; try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+\"HelloLehr\")).build(); jwt = verifier.verify(token); } catch (Exception e) { //效验失败
//这⾥抛出的异常是我⾃定义的⼀个异常,你也可以写成别的 throw new TokenUnavailable(); } }
/**
* 获取签发对象 */
public static String getAudience(String token) throws TokenUnavailable { String audience = null; try {
audience = JWT.decode(token).getAudience().get(0); } catch (JWTDecodeException j) { //这⾥是token解析失败
throw new TokenUnavailable(); }
return audience; }
/**
* 通过载荷名字获取载荷的值 */
public static Claim getClaimByName(String token, String name){ return JWT.decode(token).getClaim(name); }}
⼀点⼩说明:
关于jwt⽣成时的加密和验证⽅法:
jwt的验证其实就是验证jwt最后那⼀部分(签名部分)。这⾥在指定签名的加密⽅式的时候,还传⼊了⼀个字符串来加密,所以验证的时候不但需要知道加密算法,还需要获得这个字符串才能成功解密,提⾼了安全性。我这⾥⽤的是id来,⽐较简单,如果你想更安全⼀点,可以把⽤户密码作为这个加密字符串,这样就算是这段业务代码泄露了,也不会引发太⼤的安全问题(毕竟我的id是谁都知道的,这样令牌就可以被伪造,但是如果换成密码,只要数据库没事那就没⼈知道)关于获得载荷的⽅法:
可能有⼈会觉得奇怪,为什么不需要解密不需要verify就能够获取到载荷⾥的内容呢?原因是,本来载荷就只是⽤Base64处理了,就没有加密性,所以能直接获取到它的值,但是⾄于可不可以相信这个值的真实性,就是要看能不能通过验证了,因为最后的签名部分是和前⾯头部和载荷的内容有关联的,所以⼀旦签名验证过了,那就说明前⾯的载荷是没有被改过的。
注解类的编写
在controller层上的每个⽅法上,可以使⽤这些注解,来决定访问这个⽅法是否需要携带token,由于默认是全部检查,所以对于某些特殊接⼝需要有免验证注解免验证注解
@PassToken:跳过验证,通常是⼊⼝⽅法上⽤这个,⽐如登录接⼝import java.lang.annotation.ElementType;import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;
/**
* @author Lehr
* @create: 2020-02-03 */
@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public @interface PassToken { boolean required() default true;}
拦截器的编写
配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/**
* @author lehr
*/
@Configuration
public class JwtInterceptorConfig implements WebMvcConfigurer { @Override
public void addInterceptors(InterceptorRegistry registry) { //默认拦截所有路径
registry.addInterceptor(authenticationInterceptor()) .addPathPatterns(\"/**\"); }
@Bean
public JwtAuthenticationInterceptor authenticationInterceptor() { return new JwtAuthenticationInterceptor(); }}
拦截器
import com.auth0.jwt.interfaces.Claim;
import com.imlehr.internship.annotation.PassToken;import com.imlehr.internship.dto.AccountDTO;
import com.imlehr.internship.exception.NeedToLogin;import com.imlehr.internship.exception.UserNotExist;import com.imlehr.internship.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.method.HandlerMethod;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.lang.reflect.Method;import java.util.Map;
/**
* @author Lehr
* @create: 2020-02-03 */
public class JwtAuthenticationInterceptor implements HandlerInterceptor { @Autowired
AccountService accountService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { // 从请求头中取出 token 这⾥需要和前端约定好把jwt放到请求头⼀个叫token的地⽅ String token = httpServletRequest.getHeader(\"token\"); // 如果不是映射到⽅法直接通过
if (!(object instanceof HandlerMethod)) { return true; }
HandlerMethod handlerMethod = (HandlerMethod) object; Method method = handlerMethod.getMethod(); //检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class); if (passToken.required()) { return true; } }
//默认全部检查 else {
System.out.println(\"被jwt拦截需要验证\"); // 执⾏认证
if (token == null) {
//这⾥其实是登录失效,没token了 这个错误也是我⾃定义的,读者需要⾃⼰修改 throw new NeedToLogin(); }
// 获取 token 中的 user Name
String userId = JwtUtils.getAudience(token);
//找找看是否有这个user 因为我们需要检查⽤户是否存在,读者可以⾃⾏修改逻辑 AccountDTO user = accountService.getByUserName(userId); if (user == null) {
//这个错误也是我⾃定义的 throw new UserNotExist(); }
// 验证 token
JwtUtils.verifyToken(token, userId)
//获取载荷内容
String userName = JwtUtils.getClaimByName(token, \"userName\").asString(); String realName = JwtUtils.getClaimByName(token, \"realName\").asString();
//放⼊attribute以便后⾯调⽤
request.setAttribute(\"userName\ request.setAttribute(\"realName\
return true; }
return true; }
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception { }
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { }}
这段代码的执⾏逻辑⼤概是这样的:
⽬标⽅法是否有注解?如果有PassToken的话就不⽤执⾏后⾯的验证直接放⾏,不然全部需要验证开始验证:有没有token?没有?那么返回错误
从token的audience中获取签发对象,查看是否有这个⽤户(有可能客户端造假,有可能这个⽤户的账户被冻结了),查看⽤户的逻辑就是调⽤Service⽅法直接⽐对即可
检验Jwt的有效性,如果⽆效或者过期了就返回错误
Jwt有效性检验成功:把Jwt的载荷内容获取到,可以在接下来的controller层中直接使⽤了(具体使⽤⽅法看后⾯的代码)
接⼝的编写
这⾥设计了两个接⼝:登录和查询名字,来模拟⼀个迷你业务,其中后者需要登录之后才能使⽤,⼤致流程如下:
登录代码
/**
* ⽤户登录:获取账号密码并登录,如果不对就报错,对了就返回⽤户的登录信息 * 同时⽣成jwt返回给⽤户 *
* @return
* @throws LoginFailed 这个LoginFailed也是我⾃定义的 */
@PassToken
@GetMapping(value = \"/login\")
public AccountVO login(String userName, String password) throws LoginFailed{ try{
service.login(userName,password); }
catch (AuthenticationException e) {
throw new LoginFailed(); }
//如果成功了,聚合需要返回的信息
AccountVO account = accountService.getAccountByUserName(userName);
//给分配⼀个token 然后返回
String jwtToken = JwtUtils.createToken(account); //我的处理⽅式是把token放到accountVO⾥去了 account.setToken(jwtToken); return account; }
业务代码
这⾥列举⼀个需要登录,⽤来测试⽤户名字的接⼝(其中⽤户的名字来源于jwt的载荷部分)
@GetMapping(value = \"/username\")
public String checkName(HttpServletRequest req) { //之前在拦截器⾥设置好的名字现在可以取出来直接⽤了 String name = (String) req.getAttribute(\"userName\"); return name; }
到此这篇关于利⽤Springboot实现Jwt认证的⽰例代码的⽂章就介绍到这了,更多相关Springboot Jwt认证内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!
因篇幅问题不能全部显示,请点此查看更多更全内容