springboot/shiro-jwt权限控制集成
2018-10-22 本文已影响256人
打不开的回忆
最近整理权限,对springboot整合shiro和jwt遇到的一些问题,简要记录。这里对shiro和jwt不做过多阐述,有兴趣的可以去官网看看。传送门:shiro官网 jwt官网
本文代码地址:码云shiro-jwt
大体思路
- springboot创建rest-api提供接口
- 集成shiro,自定义filter实现认证和授权
- 集成jwt,生成token 编码token 解码token
- 正确处理认证授权抛出的异常
- 包装api统一返回值,对api全局异常处理
对shiro和jwt理解的不深,不耍流氓,直接干货了!
pom文件引入shiro和jwt已经fastjson的支持
<!-- shiro jar-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
<!-- jwt jar-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
自定义realm处理认证和授权的逻辑
/**
* 设置realm支持的authenticationToken类型
*/
@Override
public boolean supports(AuthenticationToken token) {
return null != token && token instanceof JwtToken;
}
/**
* 登陆认证
*
* @param authenticationToken jwtFilter传入的token
* @return 登陆信息
* @throws AuthenticationException 未登陆抛出异常
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws AuthenticationException {
//getCredentials getPrincipal getToken 都是返回jwt生成的token串
String token = (String) authenticationToken.getCredentials();
String username = JwtUtils.getUserName(token);
if (username == null) {
throw new AccountException("token invalid");
}
//如果需要可以根据业务实现db操作,这里根据service写死
LoginUser loginUser = loginUserService.findByUserName(username);
// if (loginUser == null) {
// throw new AuthenticationException("User didn't existed!");
// }
if (!JwtUtils.verify(username, loginUser.getPassword(), token)) {
throw new UnknownAccountException("Username or password error");
}
return new SimpleAuthenticationInfo(token, token, getName());
}
/**
* 授权认证
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String token = principalCollection.toString();
//根据token获取权限授权
String userName = JwtUtils.getUserName(token);
LoginUser loginUser = loginUserService.findByUserName(userName);
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(loginUser.getRoles());
authorizationInfo.setStringPermissions(loginUser.getPermissions());
return authorizationInfo;
}
自定义token
/**
* 参照UsernamePasswordToken,用于扩展业务,由于rest api不需要rememberMe,已丢弃
*
* @author likai
* @Date 2018/10/18
*/
public class JwtToken implements HostAuthenticationToken {
private String username;
private char[] password;
private String host;
private String token;
....setter getter省略,重写下面两个方法返回token
@Override
public Object getPrincipal() {
return this.getToken();
}
@Override
public Object getCredentials() {
return this.getToken();
}
自定义jwtFilter,重写BasicHttpAuthenticationFilter里的方法,
这里说下几个方法,首先跟踪这个Basic 的filter,一路找到PathMatchingFilter的isFilterChainContinued,对拦截器的请求链进行判断,里面调用了onPreHandle方法,AccessControlFilter对它进行了重写,查看方法是对 允许的请求和未允许的请求做预判断,isAccessAllowed方法里面最终调用了Subject.login(req,res);进行判断当前操作者是否有权限,这里我们是用token判断的,用不到,多以我们的请求都是未允许的,只需要重写onAccessDenied进行判断即可
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return this.isAccessAllowed(request, response, mappedValue) || this.onAccessDenied(request, response, mappedValue);
}
自定义filter的逻辑是关键,需要重写以下几个方法
onAccessDenied方法逻辑很简单,isLoginAttempt 根据请求header是否携带token判断是否已经登录,成功则执行executeLogin,调用realm方法进行认证token,不成功则直接sendChallenge,返回未登陆出去。具体方法已经重写,可以看代码注释,
/**
* 自定义拦截器
*
* @author likai
* @Date 2018/10/18
*/
public class JwtFilter extends BasicHttpAuthenticationFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(JwtFilter.class);
private static final String AUTHZ_HEADER = "token";
private static final String CHARSET = "UTF-8";
/**
* 处理未经验证的请求
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
boolean loggedIn = false;
if (this.isLoginAttempt(request, response)) {
loggedIn = this.executeLogin(request, response);
}
if (!loggedIn) {
this.sendChallenge(request, response);
}
return loggedIn;
}
/**
* 请求是否已经登录(携带token)
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
String authzHeader = WebUtils.toHttp(request).getHeader(AUTHZ_HEADER);
return authzHeader != null;
}
/**
* 执行登录方法(由自定义realm判断,吃掉异常返回false)
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response)
throws Exception {
String token = WebUtils.toHttp(request).getHeader(AUTHZ_HEADER);
if (null == token) {
String msg = "executeLogin method token must not be null";
throw new IllegalStateException(msg);
}
//交给realm判断是否有权限,没权限返回false交给onAccessDenied
JwtToken jwtToken = new JwtToken(token);
try {
this.getSubject(request, response).login(jwtToken);
return true;
} catch (AuthenticationException e) {
return false;
}
}
/**
* 构建未授权的请求返回,filter层的异常不受exceptionAdvice控制,这里返回401,把返回的json丢到response中
*/
@Override
protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
HttpServletResponse httpResponse = WebUtils.toHttp(response);
String contentType = "application/json;charset=" + CHARSET;
httpResponse.setStatus(Code.UNAUTHENTICATED);
httpResponse.setContentType(contentType);
try {
String msg = "对不起,您无权限进行操作!";
RestResponse unauthentication = RestResponse.newBuilder()
.setCode(Code.UNAUTHENTICATED)
.setMsg(msg).build();
PrintWriter printWriter = httpResponse.getWriter();
printWriter.append(JSON.toJSONString(unauthentication));
// byte[] data = unauthentication.toString().getBytes(CHARSET);
// OutputStream outputStream = httpResponse.getOutputStream();
// outputStream.write(data);
} catch (IOException e) {
LOGGER.error("sendChallenge error,can not resolve httpServletResponse");
}
return false;
}
/**
* 请求前处理,处理跨域
*/
@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请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
自定义了token filter realm,接下来只需要把这些自定的bean,放入shiro的配置中去,
/**
* @author likai
* @Date 2018/10/18
*/
@Configuration
public class ShiroConfig {
private static final String JWT_FILTER_NAME = "jwt";
/**
* 自定义realm,实现登录授权流程
*/
@Bean
public Realm authRealm() {
return new AuthRealm();
}
/**
* 配置securityManager 管理subject(默认),并把自定义realm交由manager
*/
@Bean
public DefaultSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(authRealm());
//非web关闭sessionManager(官网有介绍)
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator storageEvaluator = new DefaultSessionStorageEvaluator();
storageEvaluator.setSessionStorageEnabled(false);
defaultSubjectDAO.setSessionStorageEvaluator(storageEvaluator);
securityManager.setSubjectDAO(defaultSubjectDAO);
return securityManager;
}
/**
* 拦截链
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setFilters(filterMap());
shiroFilterFactoryBean.setFilterChainDefinitionMap(definitionMap());
return shiroFilterFactoryBean;
}
/**
* 自定义拦截器,处理所有请求
*/
private Map<String, Filter> filterMap() {
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put(JWT_FILTER_NAME, new JwtFilter());
return filterMap;
}
/**
* url拦截规则
*/
private Map<String, String> definitionMap() {
Map<String, String> definitionMap = new HashMap<>();
definitionMap.put("/login", "anon");
definitionMap.put("/**", JWT_FILTER_NAME);
return definitionMap;
}
/**
* 开启注解
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib代理,防止和aop冲突
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean("authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor advisor(DefaultSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
至此,shiro和jwt已经全部集成了,在controller上加入注解就可以验证了,其他代码参见git和码云:码云重送门 github重送门git挂了稍后补充地址
本文只是简单做了demo,如有不正之处请联系作者或提出Issues进行修改,万分感谢!
微信: