1.背景
- 在开发pingss-sys脚手架(项目地址)时,需要在微服务分布式环境中管理权限。有两种比较通用模式:
- 基于session,把session序列化,以实现多系统的session共享。可以采用shiro+redis实现,有现成的jar可使用
- 基于jwt,使用无状态的权限认证
- 鉴于jwt无状态的权限认证在多个平台下适用性更好,本人采用了此种模式,结合shiro实现
2.思路
3.步骤
A.实现AuthenticationToken,自定义JwtToken
/**
*********************************************************
** @desc : JwtToken
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
public class JwtToken implements AuthenticationToken {
private String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
B.编写jwt工具,实现jwt加密及验证功能
/**
*********************************************************
** @desc : JwtUtil
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
public class JwtUtil {
//**用户名称的key
private static final String USER_NAME = "userName";
//**默认的jwt加密secret
private static final String DEFAULT_SECRET = "pingssys";
//**默认的过期时间5分钟
private static final long DEFAULT_EXPIRE_TIME = 5;
/**
*********************************************************
** @desc :生成访问令牌
** @author Pings
** @date 2019/1/23
** @param secret secret
** @param userName 用户名
** @param expiresTime 过期时间
** @return String
* *******************************************************
*/
public static String sign(String secret, String userName, long expiresTime) {
//**过期时间
expiresTime = expiresTime > 0 ? expiresTime : DEFAULT_EXPIRE_TIME;
expiresTime = expiresTime * 60 * 1000;
Algorithm algorithm = Algorithm.HMAC256(getSecret(secret, userName));
return JWT.create().withClaim(USER_NAME, userName)
.withExpiresAt(new Date(currentTimeMillis + expiresTime)).sign(algorithm);
}
/**
*********************************************************
** @desc : 校验token
** @author Pings
** @date 2019/1/23
** @param token 令牌
** @param secret secret
** @return boolean
* *******************************************************
*/
public static boolean verify(String token, String secret) {
Algorithm algorithm = Algorithm.HMAC256(getSecret(secret, JwtUtil.getUserName(token)));
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
}
/**
*********************************************************
** @desc : 获取用户名称
** @author Pings
** @date 2019/1/23
** @param token 令牌
** @return String
* *******************************************************
*/
public static String getUserName(String token) {
Claim claim = decodeToken(token, jwt -> jwt.getClaim(USER_NAME));
return claim == null ? null : claim.asString();
}
/**
*********************************************************
** @desc :把访问令牌存放到响应的头信息中
** @author Pings
** @date 2019/3/21
** @param response 响应
** @param token 令牌
* *******************************************************
*/
public static void setHttpServletResponse(HttpServletResponse response, String token) {
response.setHeader("Authorization", token);
response.setHeader("Access-Control-Expose-Headers", "Authorization");
}
/**
*********************************************************
** @desc : token解码
** @author Pings
** @date 2019/1/23
** @param token 标记
** @return T
* *******************************************************
*/
private static <T> T decodeToken(String token, Function<DecodedJWT, T> func) {
try {
DecodedJWT jwt = JWT.decode(token);
return func.apply(jwt);
} catch (JWTDecodeException e) {
return null;
}
}
//**获取jwt加密secret
private static String getSecret(String secret, String userName){
return userName + (StringUtils.isNotBlank(secret) ? secret : DEFAULT_SECRET);
}
}
C.自定义shiro filter
/**
*********************************************************
** @desc : JwtFilter
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**登录认证*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//**判断用户是否要登入
if (this.isLoginAttempt(request, response)) {
try {
//**登录认证
return this.executeLogin(request, response);
} catch (Exception e) {
this.response401(request, response, e.getMessage());
return false;
}
}
return true;
}
/**去掉调用executeLogin,避免循环调用doGetAuthenticationInfo方法*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
this.sendChallenge(request, response);
return false;
}
/**检测Header里面是否包含Authorization字段*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
String token = this.getAuthzHeader(request);
return token != null;
}
/**调用JwtRealm进行登录认证*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
//**获取token
JwtToken token = new JwtToken(this.getAuthzHeader(request));
//**提交给JwtRealm认证
this.getSubject(request, response).login(token);
//**没有抛出异常则代表登入成功
return true;
}
/**401时直接返回Response信息*/
private void response401(ServletRequest req, ServletResponse resp, String msg) {
HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
ApiResponse response = new ApiResponse(HttpStatus.UNAUTHORIZED.value(), "Unauthorized: " + msg, null);
String data = JSONObject.toJSONString(response);
try(PrintWriter out = httpServletResponse.getWriter()) {
out.append(data);
} catch (IOException e) {
throw new RuntimeException(e.getMessage());
}
}
/**支持跨域*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
//**跨域时会首先发送一个OPTIONS请求,返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
D.自定义shiro realm
/**
*********************************************************
** @desc : 自定义Realm
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
public class JwtRealm extends AuthorizingRealm {
@Value("${sys.jwt.secret}")
private String secret;
@Reference(version = "${sys.service.version}")
private UserService userService;
/**必须重写此方法,不然Shiro会报错*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**权限验证*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
String userName = JwtUtil.getUserName(principals.toString());
//**获取用户
User user = this.userService.getByUserName(userName);
//**用户角色
Set<String> roles = user.getRoles().stream().map(Role::getCode).collect(toSet());
authorizationInfo.addRoles(roles);
//**用户权限
Set<String> rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet());
authorizationInfo.addStringPermissions(rights);
return authorizationInfo;
}
/**登录验证*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
String token = (String) auth.getCredentials();
//**获取用户名称
String userName = JwtUtil.getUserName(token);
//**用户名称为空
if (StringUtils.isBlank(userName)) {
throw new AuthenticationException("The account in Token is empty.");
}
//**获取用户
User user = this.userService.getByUserName(userName);
if (user == null) {
throw new AuthenticationException("The account does not exist.");
}
//**登录认证
if (JwtUtil.verify(token, userName, secret)) {
return new SimpleAuthenticationInfo(token, token, "jwtRealm");
}
throw new AuthenticationException("Username or password error.");
}
/**管理员不验证权限*/
@Override
public boolean isPermitted(PrincipalCollection principal, String permission){
AuthorizationInfo info = this.getAuthorizationInfo(principal);
return info.getRoles().contains("admin") || super.isPermitted(principal, permission);
}
/**管理员不验证角色*/
@Override
public boolean hasRole(PrincipalCollection principal, String roleIdentifier) {
AuthorizationInfo info = this.getAuthorizationInfo(principal);
return info.getRoles().contains("admin") || super.hasRole(principal, roleIdentifier);
}
}
E.配置shrio
/**
*********************************************************
** @desc : Shiro配置
** @author Pings
** @date 2019/1/23
** @version v1.0
* *******************************************************
*/
@Configuration
public class ShiroConfig {
@Bean
public JwtRealm jwtRealm(){
return new JwtRealm();
}
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//**使用自定义JwtRealm
manager.setRealm(jwtRealm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//**添加自定义过滤器jwt
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
//**自定义url规则
Map<String, String> filterRuleMap = new LinkedHashMap<>();
//不拦截请求swagger-ui页面请求
filterRuleMap.put("/webjars/**", "anon");
//jwt过滤器拦截请求
filterRuleMap.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
F.LoginController中编写登录方法
/**
*********************************************************
** @desc : 登录
** @author Pings
** @date 2019/1/22
** @param userName 用户名称
** @param password 用户密码
** @return ApiResponse
* *******************************************************
*/
@ApiOperation(value="登录", notes="验证用户名和密码")
@PostMapping(value = "/account")
public ApiResponse account(String userName, String password, HttpServletResponse response){
if(StringUtils.isBlank(userName) || StringUtils.isBlank(password))
throw new UnauthorizedException("用户名/密码不能为空");
//**md5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
User user = this.userService.getByUserName(userName);
if(user != null && user.getPassword().equals(password)) {
JwtUtil.setHttpServletResponse(response, JwtUtil.sign(userName, password, expireTime));
//**用户权限
Set<String> rights = user.getRoles().stream().map(Role::getRights).flatMap(List::stream).map(Right::getCode).collect(toSet());
return new ApiResponse(200, "登录成功", rights);
} else
return new ApiResponse(500, "用户名/密码错误");
}
4.说明
- dubbo分布式系统权限认证
- 只要dubbo多个子系统签发token的方式相同,某个子系统签发的token即可访问所有其它的子系统
- 存在的问题
- 生成的token如果过期时间太短,则每次到期后,都需要用户重新登录
- 生成的token如果过期时间太长,由于token签发后,在有效期内无法注销,存在安全隐患
- 下一篇结合RefreshToken和AccessToken一起使用,解决上述两个问题