废话不多说,直接上代码。
首先,我将shiro鉴权和jwt认证做成了一个微服务:lls-base-shiro。
关键pom如下:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring-boot-web-starter.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>${java-jwt.version}</version>
</dependency>
shiroconfig如下:
package com.lenovoedu.config;
import com.lenovoedu.interceptor.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 项目名称:lls2.0
* 类名称:ShiroConfig
* 类描述:shiro配置类
* 创建人:YuanGL
* 创建时间:2019年4月1日14:12:36
* version 2.0
*/
@Configuration
public class ShiroConfig {
/**
* 注入安全过滤器
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// setLoginUrl 如果不设置值,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/notLogin");
// 设置无权限时跳转的 url;
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
// filterChainDefinitionMap.put("/**", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
//前后端带login登录的或者其他登录的通通放行
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/v2/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
filterChainDefinitionMap.put("/loginjwt", "anon");
filterChainDefinitionMap.put("/websocket/**", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 注入安全管理器
* @param myRealm
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(ShiroRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 自定义身份认证 realm;
* <p>
* 必须写这个类,并加上 @Bean 注解,目的是注入 CustomRealm,
* 否则会影响 CustomRealm类 中其他类的依赖注入
*/
@Bean
public ShiroRealm shiroRealm() {
return new ShiroRealm();
}
/**
* LifecycleBeanPostProcessor将Initializable和Destroyable的实现类统一在其内部
* 自动分别调用了Initializable.init()和Destroyable.destroy()方法,从而达到管理shiro bean生命周期的目的。
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
ShiroRealm如下:
package com.lenovoedu.config;
import com.lenovoedu.common.JwtToken;
import com.lenovoedu.model.sys.model.SysUser;
import com.lenovoedu.shiro.service.IPermitService;
import com.lenovoedu.utils.JWTGenerator;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Set;
/**
* 项目名称:lls2.0
* 类名称:TraxexShiroRealm
* 类描述:shiro AuthorizingRealm继承实现
* 创建人:YuanGL
* 创建时间:2019年4月1日16:17:46
* version 2.0
*/
public class ShiroRealm extends AuthorizingRealm {
private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
@Autowired
private IPermitService permitService;
@Autowired
private JWTGenerator jwtGenerator;
/**
* 必须重写此方法,不然Shiro会报错
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权
* @param principals 用户信息
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
}
String email = JWTGenerator.getEmail(principals.toString());
Set<String> roles = permitService.getSysUserRoles(email);
Set<String> permits = permitService.getSysUserAuthCodes(email);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles);
info.setStringPermissions(permits);
return info;
}
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
* @param auth 用户权限信息
* @return AuthenticationInfo
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String token = (String) auth.getCredentials();
String email = JWTGenerator.getEmail(token);
if (email == null) {
throw new AuthenticationException("token无效");
}
SysUser sysUser = permitService.getUserByLoginName(email);
if(sysUser==null){
throw new AuthenticationException("用户不存在!");
}
// 检查用户状态
if(!sysUser.getIsAble().equals("Y")) {
throw new AuthenticationException("用户已删除!");
}
if(!jwtGenerator.requireTokenBoolean(request,sysUser.getPassword())){
throw new AuthenticationException("用户名或密码错误(token无效或者与登录者不匹配)!");
}
return new SimpleAuthenticationInfo(token, token, "shiro_realm");
}
}
JwtToken如下:
package com.lenovoedu.common;
import org.apache.shiro.authc.AuthenticationToken;
/**
* JwtToken:实现shiro的AuthenticationToken接口的类JwtToken
*
* @author zhangxiaoxiang
* @date: 2019/07/12
*/
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;
}
}
JwtFilter如下:(jwt的拦截器)
package com.lenovoedu.interceptor;
import com.lenovoedu.common.JwtToken;
import com.lenovoedu.constants.BackendYMLConstants;
import com.lenovoedu.utils.StringPool;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.Filter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* JwtFilter:jwt过滤器来作为shiro的过滤器
* 方法执行顺序分别为:isAccessAllowed(里面的isLoginRequest实现了isLoginAttempt),isLoginAttempt,executeLogin。
* 验证不成功会执行onAccessDenied
*
* @author ygl
* @date: 2019/07/12
*/
@Component//这个注入与否影响不大
public class JwtFilter extends BasicHttpAuthenticationFilter implements Filter {
/**
* 处理未经验证的请求
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.setStatus(401);
return false;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
// 原用来判断是否是登录请求,在本例中不会拦截登录请求,用来检测Header中是否包含 JWT token 字段
if (this.isLoginRequest(request, response)){
return false;
}
boolean allowed = false;
try {
// 检测Header里的 JWT token内容是否正确,尝试使用 token进行登录
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { // not found any token
} catch (Exception e) {
}
return allowed || super.isPermissive(mappedValue);
}
/**
* 判断用户是否想要登入。
* 检测 header 里面是否包含 Token 字段
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(StringPool.HEADER_TOKEN);
return token == null;
}
/**
* 执行登录
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(BackendYMLConstants.HEADER_TOKEN);
if (null == token) {
String msg = "executeLogin method token must not be null";
throw new IllegalStateException(msg);
}
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
try {
// 这个方法会执行到realm的认证方法
this.getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
} catch (AuthenticationException e) {
return false;
}
}
/**
* 对跨域提供支持
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
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"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
Jwt的工具类如下:
package com.lenovoedu.utils;
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.DecodedJWT;
import com.lenovoedu.base.log.Logger;
import com.lenovoedu.base.log.LoggerFactory;
import com.lenovoedu.constants.BackendYMLConstants;
import com.lenovoedu.exception.DescribeException;
import com.lenovoedu.exception.ExceptionEnum;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JWTGenerator {
private static Logger logger = LoggerFactory.getLogger(JWTGenerator.class);
private final static Map<String, Object> JWT_HEADER = new HashMap<String, Object>(){
{
put("alg", "HmacSHA256");
put("typ", "JWT");
}
};
public final static String USER_ID = "userId";
public final static String EMAIL = "email";
@Value(BackendYMLConstants.JWT_TOKEN_SECRET)
public String jwtTokenSecret;
@Value(BackendYMLConstants.JWT_AUDIENCE)
public String jwtAudience;
@Value(BackendYMLConstants.JWT_ISS_USER)
public String jwtIssUser;
@Value(BackendYMLConstants.JWT_SUBJECT)
public String jwtSubject;
@Value(BackendYMLConstants.JWT_EXPIRES_TIME)
public Long tokenExpireTime;
public String createToken(HttpServletRequest request, String email,String password) throws UnsupportedEncodingException {
String ua = request.getHeader(StringPool.USER_AGENT);
if (StringUtils.isNotEmpty(ua)) {
return JWT.create()
.withHeader(JWT_HEADER)
.withClaim(EMAIL, email)
.withExpiresAt(new Date(System.currentTimeMillis() + tokenExpireTime))
.withNotBefore(new Date())
.withIssuer(jwtIssUser)
.withSubject(jwtSubject)
.withAudience(jwtAudience)
.sign(Algorithm.HMAC256(password + ua));
}
return null;
}
public DecodedJWT requireToken(HttpServletRequest request,String password) throws UnsupportedEncodingException {
String ua = request.getHeader(StringPool.USER_AGENT);
String token = request.getHeader(StringPool.HEADER_TOKEN);
logger.info("request token:{}", token);
if (StringUtils.isNotBlank(ua)
&& StringUtils.isNotBlank(token)) {
Algorithm algorithm = Algorithm.HMAC256(password + ua);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(jwtIssUser)
.withSubject(jwtSubject)
.withAudience(jwtAudience)
.acceptExpiresAt(0)
.acceptNotBefore(0)
.build(); //Reusable verifier instance
return verifier.verify(token);
}
throw new DescribeException(ExceptionEnum.NEED_LOGIN_ERROR);
}
public boolean requireTokenBoolean(HttpServletRequest request,String password){
try {
String ua = request.getHeader(StringPool.USER_AGENT);
String token = request.getHeader(StringPool.HEADER_TOKEN);
logger.info("request token:{}", token);
if (StringUtils.isNotBlank(ua)
&& StringUtils.isNotBlank(token)) {
Algorithm algorithm = null;
algorithm = Algorithm.HMAC256(password + ua);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(jwtIssUser)
.withSubject(jwtSubject)
.withAudience(jwtAudience)
.acceptExpiresAt(0)
.acceptNotBefore(0)
.build(); //Reusable verifier instance
DecodedJWT verify = verifier.verify(token);
return true;
}
return false;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return false;
}
}
/**
*
* @return token中包含的用户信息------》email
*/
public static String getEmail(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(EMAIL).asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
配置zuul的前置过滤器如下:
package com.lenovoedu.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 第二个前置过滤器,用来判断是否需要继续转发
* @author ygl
* @date: 2020年3月22日23:45:56
*/
@Component
public class SecondFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(SecondFilter.class);
@Override
public String filterType() {
/*
pre:可以在请求被路由之前调用
route:在路由请求时候被调用
post:在route和error过滤器之后被调用
error:处理请求时发生错误时被调用
* */
// 前置过滤器
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
优先级为0,数字越大,优先级越低
return -1;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
return true;
}
@Override
public Object run() {
logger.debug("*****************FirstFilter run start*****************");
System.out.println("*****************FirstFilter run start*****************");
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Headers","x-requested-with, authorization, content-type");
response.setHeader("Access-Control-Allow-Methods","POST,GET");
response.setHeader("Access-Control-Expose-Headers","X-forwared-port, X-forwarded-host, Content-Disposition");
response.setHeader("Vary","Origin,Access-Control-Request-Method,Access-Control-Request-Headers");
String ua = request.getHeader("User-Agent");
String token = request.getHeader("token");
String url = request.getRequestURL().toString();
logger.info("request token:{}", token);
if ((StringUtils.isNotBlank(ua) && StringUtils.isNotBlank(token)) || url.contains("loginjwt") || url.contains("swagger")|| url.contains("v2") || url.contains("websocket")) {
//允许继续路由
ctx.addZuulRequestHeader("token", token);
ctx.addZuulRequestHeader("User-Agent", ua);
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
}else{
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
}
logger.debug("*****************FirstFilter run end*****************");
return null;
}
}
这样就已经实现了jwt的认证和鉴权。鉴权的话,只需要在相应的方法上添加@RequiresPermissions或者@RequiresRole注解即可。