JAVA++:JWT 实现统一的认证授权
概述:
以下代码仅供参考:在实际开发中用什么的都有(都存在优点与缺点)提供一种设计理念思想;
根据实际场景 可以设计出自己的一套认证规则
主要类说明:
JwtCheck.java --> JwtToken校验注解
JwtCheckAop.java --> JwtToken校验注解AOP
JwtTokenFilter.java --> (基于GateWay)自定义JWT 过滤器
AuthController.java -->认证测试接口
application.yml -->配置文件
JwtUtil.java --> jwt工具类
加入 jjwt 依赖:
jjwt 是一个Java对jwt的支持库,我们使用这个库来创建、解码token
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
JwtUtil :工具类
import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.apache.commons.codec.binary.Base64; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * 描述: jwt 工具类 */ public class JwtUtil { //密钥 -- 根据实际项目,这里可以做成配置 public static final String KEY = "022bdc63c3c5a45879ee6581508b9d03adfec4a4658c0ab3d722e50c91a351c42c231cf43bb8f86998202bd301ec52239a74fc0c9a9aeccce604743367c9646b"; /** * 由字符串生成加密key * * @return */ public static SecretKey generalKey(){ byte[] encodedKey = Base64.decodeBase64(KEY); SecretKeySpec key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } /** * 创建jwt * @param id * @param issuer * @param subject * @param ttlMillis * @return * @throws Exception */ public static String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception { // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成JWT的时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) Map<String, Object> claims = new HashMap<>(); claims.put("uid", "123456"); claims.put("user_name", "admin"); claims.put("nick_name", "X-rapido"); // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。 // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。 SecretKey key = generalKey(); // 下面就是在为payload添加各种标准声明和私有声明了 JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body .setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setId(id) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。 .setIssuedAt(now) // iat: jwt的签发时间 .setIssuer(issuer) // issuer:jwt签发人 .setSubject(subject) // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。 .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥 // 设置过期时间 if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); } return builder.compact(); } /** * 解密jwt * * @param jwt * @return * @throws Exception */ public static Claims parseJWT(String jwt) throws Exception { SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样 Claims claims = Jwts.parser() //得到DefaultJwtParser .setSigningKey(key) //设置签名的秘钥 .parseClaimsJws(jwt).getBody(); //设置需要解析的jwt return claims; } }
基于 gateway 编写的过滤器 JwtTokenFilter :
getOrder方法中的返回值的数据越小,过滤器的级别越高
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.lzx.gateway.dto.ReturnData; import com.lzx.gateway.jwt.JwtUtil; import io.jsonwebtoken.ExpiredJwtException; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.StringUtils; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Arrays; /** * 描述: JwtToken 过滤器 */ @Component //读取 yml 文件下的 org.my.jwt @ConfigurationProperties("org.my.jwt") @Setter @Getter @Slf4j public class JwtTokenFilter implements GlobalFilter,Ordered { private String[] skipAuthUrls; private ObjectMapper objectMapper; public JwtTokenFilter(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } /** * 过滤器 * @param exchange * @param chain * @return */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String url = exchange.getRequest().getURI().getPath(); //跳过不需要验证的路径 if(null != skipAuthUrls&&Arrays.asList(skipAuthUrls).contains(url)){ return chain.filter(exchange); } //获取token String token = exchange.getRequest().getHeaders().getFirst("Authorization"); ServerHttpResponse resp = exchange.getResponse(); if(StringUtils.isBlank(token)){ //没有token return authErro(resp,"请登陆"); }else{ //有token try { JwtUtil.checkToken(token,objectMapper); return chain.filter(exchange); }catch (ExpiredJwtException e){ log.error(e.getMessage(),e); if(e.getMessage().contains("Allowed clock skew")){ return authErro(resp,"认证过期"); }else{ return authErro(resp,"认证失败"); } }catch (Exception e) { log.error(e.getMessage(),e); return authErro(resp,"认证失败"); } } } /** * 认证错误输出 * @param resp 响应对象 * @param mess 错误信息 * @return */ private Mono<Void> authErro(ServerHttpResponse resp,String mess) { resp.setStatusCode(HttpStatus.UNAUTHORIZED); resp.getHeaders().add("Content-Type","application/json;charset=UTF-8"); ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess); String returnStr = ""; try { returnStr = objectMapper.writeValueAsString(returnData); } catch (JsonProcessingException e) { log.error(e.getMessage(),e); } DataBuffer buffer = resp.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8)); return resp.writeWith(Flux.just(buffer)); } @Override public int getOrder() { return -100; } }
添加认证的api接口:
这里为了方便测试,认证的接口写在了网关的项目中,实际生产可以把接口设计在专门的认证服务中
/** * 登陆认证接口 * @param userDTO * @return */ @PostMapping("/login") public ReturnData<String> login(@RequestBody UserDTO userDTO) throws Exception { ArrayList<String> roleIdList = new ArrayList<>(1); roleIdList.add("role_test_1"); JwtModel jwtModel = new JwtModel("test", roleIdList); int effectivTimeInt = Integer.valueOf(effectiveTime.substring(0,effectiveTime.length()-1)); String effectivTimeUnit = effectiveTime.substring(effectiveTime.length()-1,effectiveTime.length()); String jwt = null; switch (effectivTimeUnit){ case "s" :{ //秒 jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 1000L); break; } case "m" :{ //分钟 jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 1000L); break; } case "h" :{ //小时 jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 60L * 60L * 1000L); break; } case "d" :{ //小时 jwt = JwtUtil.createJWT("test", "test", objectMapper.writeValueAsString(jwtModel), effectivTimeInt * 24L * 60L * 60L * 1000L); break; } } return new ReturnData<String>(HttpStatus.SC_OK,"认证成功",jwt); }
yml配置文件:
这里读取了配置中心的文件,大家可以根据自己的需求更改
################################### #服务启动端口的配置 ################################### server: port: ${server-port} ############################################################### # eureka 的相关配置 # 如果不需要 结合eureka 使用,可以不要这一段配置 ############################################################### eureka: client: fetch-registry: true register-with-eureka: ${register-with-eureka} # 是否注册到eureka service-url: defaultZone: ${service-url-defaultZone} instance: prefer-ip-address: false hostname: ${instance-hostname} spring: cloud: ################################# # gateway相关配置 ################################# gateway: # 路由定义 routes: - id: baidu uri: https://www.baidu.com predicates: - Path=/baidu/** filters: - StripPrefix=1 - id: eureka-manage uri: lb://eureka-manage predicates: - Path=/eureka-manage/** filters: - StripPrefix=1 - id: sina uri: https://www.sina.com.cn/ predicates: - Path=/sina/** filters: - StripPrefix=1 org: my: jwt: #跳过认证的路由 skip-auth-urls: - /baidu ############################################ # 有效时长 # 单位:d:天、h:小时、m:分钟、s:秒 ########################################### effective-time: 1m
测试:
直接不带认证信息访问一个需要认证的路由:访问一个新浪得路由,提示需要认证
调用认证api获取token
把token加入请求头,再次访问新浪得路由,可以通过认证
尝试token过期后访问,在application.yml中我配置了token一分钟后过期,一分钟后我再次携带token访问新浪得路由,提示认证过期。
进阶:制作JwtToken校验注解:
import java.lang.annotation.*; /** * 描述: jwt检查注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface JwtCheck { String value() default ""; }
定义注解得AOP:
import com.fasterxml.jackson.databind.ObjectMapper; import com.lzx.gateway.dto.ReturnData; import com.lzx.gateway.jwt.JwtUtil; import io.jsonwebtoken.ExpiredJwtException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestHeader; import java.lang.annotation.Annotation; import java.lang.reflect.Method; /** * 描述:添加了 JwtCheck 注解 的Aop */ @Component @Aspect @Slf4j public class JwtCheckAop { @Autowired private ObjectMapper objectMapper; @Pointcut("@annotation(com.lzx.gateway.annotation.JwtCheck)") private void apiAop(){ } /** * 方法执行前的aop * @param point * @return * @throws Throwable */ @Around("apiAop()") public Object aroundApi(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); //获取参数上得所有注解 Annotation[][] parameterAnnotationArray = method.getParameterAnnotations(); Object[] args = point.getArgs(); String token = null; /* a -> start 这个代码片得逻辑:找出有 @RequestHeader("Authorization") 的参数,赋值给 token变量 */ for(Annotation[] annotations : parameterAnnotationArray){ for(Annotation a:annotations){ if(a instanceof RequestHeader){ RequestHeader requestHeader = (RequestHeader)a; if("Authorization".equals(requestHeader.value())){ token = (String) args[ArrayUtils.indexOf(parameterAnnotationArray,annotations)]; } } } } /* a -> end */ if(StringUtils.isBlank(token)){ //没有token return authErro("请登陆"); }else{ //有token try { JwtUtil.checkToken(token,objectMapper); Object proceed = point.proceed(); return proceed; }catch (ExpiredJwtException e){ log.error(e.getMessage(),e); if(e.getMessage().contains("Allowed clock skew")){ return authErro("认证过期"); }else{ return authErro("认证失败"); } }catch (Exception e) { log.error(e.getMessage(),e); return authErro("认证失败"); } } } /** * 认证错误输出 * @param mess 错误信息 * @return */ private Object authErro(String mess) { ReturnData<String> returnData = new ReturnData<>(org.apache.http.HttpStatus.SC_UNAUTHORIZED, mess, mess); return returnData; } }
注解的使用方法:
直接在方法上使用 @JwtCheck
/** * jwt 检查注解测试 测试 * @return */ @GetMapping("/testJwtCheck") @JwtCheck public ReturnData<String> testJwtCheck(@RequestHeader("Authorization")String token,@RequestParam("name")@Valid String name){ return new ReturnData<String>(HttpStatus.SC_OK,"请求成功咯","请求成功咯"+name); }
没有十全十美的技术,都会有 缺点和优点,根据实际情况而定。