前言
前一段时间在看《Spring Boot与Kubernetes云原生微服务实践》这门课,正好团队也在用SpringCloud技术框架,下一个项目可能也会用到JWTi相关的东西,特别提前做一下技术穿刺和清扫一下障碍。本文将结合SpringCloud整体框架以及Zuul作为网关来实现JWT的统一认证和鉴权的实现方式。
预备知识
- SpringCloud 背景知识,受篇幅所限,项目中所需要的注册中心,配置中心等服务都没有明确说明以及启动类也没有给出
- Zuul基本知识,比如如何注册过滤器,还有一些基本概念也没有介绍
总体概述
《Spring Boot与Kubernetes云原生微服务实践》课程主要讲解了如下的认证设计
但实际上我们项目都会涉及到角色和组织机构相关信息,所以更多的用到的是如下的认证设计。
本文介绍的实现思路就是上图的一种简化。简化的地方是我们直接是将组织机构信息以及角色信息集成到用户信息中。
实际代码
网关
Auth.java
package com.zew.se2.gateway.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.zew.se2.utils.auth.Sessions;
import com.zew.se2.utils.auth.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
/**
* @author zhaoe
*/
@Slf4j
public class AuthFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String requestUri = request.getRequestURI();
log.info("请求的URI:{}",requestUri);
if(requestUri.contains("/login")&&request.getMethod().equalsIgnoreCase("post")){
log.info("用户登录请求");
currentContext.setSendZuulResponse(true);
return null;
}else {
UserDto userDto=Sessions.verifyRequest(request);
if(userDto==null){
currentContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
currentContext.setSendZuulResponse(false);
return null;
}
// 根据访问的URL与userDto进行权限校验
if(isValid(request,userDto)){
log.info("用户通过验证,{}",userDto);
currentContext.setSendZuulResponse(true);
}else {
currentContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
currentContext.setSendZuulResponse(false);
}
}
return null;
}
private boolean isValid(HttpServletRequest request, UserDto userDto) {
return true;
}
}
认证服务
LoginController.java
package com.zew.se2.sim.ctrl;
import com.zew.se2.utils.HelperService;
import com.zew.se2.utils.auth.AuthConstant;
import com.zew.se2.utils.auth.AuthContext;
import com.zew.se2.utils.auth.Sessions;
import com.zew.se2.utils.auth.dto.OrgDto;
import com.zew.se2.utils.auth.dto.RoleDto;
import com.zew.se2.utils.auth.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author zew
*/
@Slf4j
@RestController
@RequestMapping("/login")
public class LoginController {
@PostMapping
public String login(@RequestParam(value = "userName", required = true) String userName,
@RequestParam(value = "password", required = true) String password,
HttpServletRequest request,
HttpServletResponse response) {
if (!StringUtils.isEmpty(AuthContext.getAuthz()) && !AuthConstant.AUTHORIZATION_ANONYMOUS_WEB.equals(AuthContext.getAuthz())) {
String url = HelperService.buildUrl("http", "index.html");
return "redirect:" + url;
}
//todo: 等待验证,通过后进入下面流程
try {
UserDto userDto=new UserDto();
userDto.setUserId("userId");
userDto.setOrgInfo(new OrgDto());
userDto.setRoleInfo(new RoleDto());
Sessions.loginUser(userDto,
true,
"123",
"",
response);
} catch (Exception e) {
log.error("验证异常",e);
return "/login";
}
return "/index";
}
}
TestController.java
package com.zew.se2.sim.ctrl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author zhaoe
*/
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping
public String hello(){
return "hello";
}
}
核心依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--下面是公共依赖-->
<dependency>
<groupId>com.zew.se2.platform</groupId>
<artifactId>utils</artifactId>
</dependency>
</dependencies>
通用服务
UserDto.java
@Data
public class UserDto {
private String userId;
private OrgDto orgInfo;
private RoleDto roleInfo;
}
Sign.java
package com.zew.se2.utils.auth;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.zew.se2.utils.auth.dto.OrgDto;
import com.zew.se2.utils.auth.dto.RoleDto;
import com.zew.se2.utils.auth.dto.UserDto;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author zew
*/
@Slf4j
public class Sign {
private static final String CLAIM_USER_ID = "userId";
private static final String CLAIM_ORG_INFO = "orgInfo";
private static final String CLAIM_ROLE_INFO = "roleInfo";
private static Map<String, JWTVerifier> verifierMap = new HashMap<>();
private static Map<String, Algorithm> algorithmMap = new HashMap<>();
private static Algorithm getAlgorithm(String signingToken) {
Algorithm algorithm = algorithmMap.get(signingToken);
if (algorithm == null) {
synchronized (algorithmMap) {
algorithm = algorithmMap.get(signingToken);
if (algorithm == null) {
algorithm = Algorithm.HMAC512(signingToken);
algorithmMap.put(signingToken, algorithm);
}
}
}
return algorithm;
}
public static DecodedJWT verifySessionToken(String tokenString, String signingToken) {
return verifyToken(tokenString, signingToken);
}
public static UserDto verifyJwt(String jwtToken, String signToken){
try{
DecodedJWT jwt = Sign.verifySessionToken(jwtToken, signToken);
UserDto userDto=new UserDto();
userDto.setUserId(jwt.getClaim(CLAIM_USER_ID).asString());
String orgDtoStr = jwt.getClaim(CLAIM_ORG_INFO).asString();
OrgDto orgDto= (OrgDto) JSONObject.toBean(JSONObject.fromObject(orgDtoStr),OrgDto.class);
userDto.setOrgInfo(orgDto);
String roleDtoStr = jwt.getClaim(CLAIM_ROLE_INFO).asString();
RoleDto roleDto= (RoleDto) JSONObject.toBean(JSONObject.fromObject(roleDtoStr),RoleDto.class);
userDto.setRoleInfo(roleDto);
return userDto;
}catch (Exception e){
log.error("JWT 校验异常 {}",jwtToken,e);
return null;
}
}
private static DecodedJWT verifyToken(String tokenString, String signingToken) {
JWTVerifier verifier = verifierMap.get(signingToken);
if (verifier == null) {
synchronized (verifierMap) {
verifier = verifierMap.get(signingToken);
if (verifier == null) {
Algorithm algorithm = Algorithm.HMAC512(signingToken);
verifier = JWT.require(algorithm).build();
verifierMap.put(signingToken, verifier);
}
}
}
DecodedJWT jwt = verifier.verify(tokenString);
return jwt;
}
public static String generateSessionToken(String userId, String signingToken, String orgInfo,String roleInfo, long duration) throws Exception{
if (StringUtils.isEmpty(signingToken)) {
throw new Exception("No signing token present");
}
Algorithm algorithm = getAlgorithm(signingToken);
String token = JWT.create()
.withClaim(CLAIM_USER_ID, userId)
.withClaim(CLAIM_ROLE_INFO, roleInfo)
.withClaim(CLAIM_ORG_INFO, orgInfo)
.withExpiresAt(new Date(System.currentTimeMillis() + duration))
.sign(algorithm);
return token;
}
}
Sessions.java
package com.zew.se2.utils.auth;
import com.zew.se2.utils.auth.dto.UserDto;
import net.sf.json.JSONObject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* @author zew
*/
public class Sessions {
public static final long SHORT_SESSION = TimeUnit.HOURS.toMillis(12);
public static final long LONG_SESSION = TimeUnit.HOURS.toMillis(30 * 24);
public static void loginUser(UserDto userDto,
boolean rememberMe,
String signingSecret,
String externalApex,
HttpServletResponse response) throws Exception {
long duration;
int maxAge;
if (rememberMe) {
// "Remember me"
duration = LONG_SESSION;
} else {
duration = SHORT_SESSION;
}
maxAge = (int) (duration / 1000);
String token = Sign.generateSessionToken(userDto.getUserId(),
signingSecret,
JSONObject.fromObject(userDto.getOrgInfo()).toString(),
JSONObject.fromObject(userDto.getRoleInfo()).toString(),
duration);
Cookie cookie = new Cookie(AuthConstant.COOKIE_NAME, token);
cookie.setPath("/");
cookie.setDomain(externalApex);
cookie.setMaxAge(maxAge);
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
public static UserDto verifyRequest(HttpServletRequest request) {
return Sign.verifyJwt(getToken(request), "123");
}
public static String getToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null || cookies.length == 0) {
return null;
}
Cookie tokenCookie = Arrays.stream(cookies)
.filter(cookie -> AuthConstant.COOKIE_NAME.equals(cookie.getName()))
.findAny().orElse(null);
if (tokenCookie == null) {
return null;
}
return tokenCookie.getValue();
}
public static void logout(String externalApex, HttpServletResponse response) {
Cookie cookie = new Cookie(AuthConstant.COOKIE_NAME, "");
cookie.setPath("/");
cookie.setMaxAge(0);
cookie.setDomain(externalApex);
response.addCookie(cookie);
}
}
核心依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.6.0</version>
</dependency>
总结
基本思路就是Zuul作为网关用来进行统一的认证和鉴权,这个鉴权包含了组织级鉴权也就是横向安全,还有功能级鉴权也就是纵向鉴权。通过URL来区分是否需要鉴权。