目录
JWT
Shiro
实现过程
1.Springboot环境搭建
2.实现JwtToken、JwtUtil、JwtFilter、这三个类
最近在做前后分离项目,前端验证用到了JWT,后端用的shiro做权限验证,基于springboot实现JWT+Shiro鉴权。
JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。这是一个完整的token,分别包含Header:头部,Payload:负载,Signature:签名
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
三个核心组件:Subject, SecurityManager 和 Realms.
Subject代表了当前用户的安全操作,即“当前操作用户”。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
ShiroBasicArchitecture
简单的说一下实现的过程,用户登录验证通过以后会返回一个token,这个token包含了用户的基本信息,前端每次的请求中header中都会携带这个token ,后端的Shiro来验证token的有效性及权限。
SpringBoot 2.2.5.RELEASE
org.apache.shiro
shiro-spring
1.5.3
com.auth0
java-jwt
3.10.3
JwtToken基于AuthenticationToken这个类实现,作用就是Shiro的用户验证token替换为基于JWT生成的toekn
JwtToken类
package com.tdrc.common.core.shiro;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @author dpf
* @version 1.0
* @date 2020-5-22 15:22
* @instruction ...
*/
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;
}
}
JwtUtil是生成、解析、验证token的工具类,这里就不贴代码,JWT官网有详细的文档,网上也有许多的例子。
JwtFilter是BasicHttpAuthenticationFilter实现的过滤器,过滤器中主要实现了这四个方法isLoginAttempt、executeLogin、isAccessAllowed、preHandle
isLoginAttempt,判断用户是否需要登录,这里做了token的非空验证,默认是所有的前端请求都必须携带token。
/**
* 判断用户是否想要登入。
* 检测header里面是否包含Authorization字段即可
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse)response;
httpServletRequest.getHeaderNames();
String access_token = httpServletRequest.getHeader("access-token");
//这只判断access_token是否为空
if(!StringUtils.isEmpty(access_token)){
access_token = access_token.substring(7);
// 判断token是否过期
log.info("判断用户是否想要登录:{}",access_token);
return true;
}else{
HttpServletResponseUtil.printJson(httpServletResponse,new JsonResult(ResultCode.PERMISSION_UNAUTHORISE));
return false;
}
}
executeLogin,获取token并执行登录的过程,具体是交给Shiro的realm来实现登录验证
/**
*执行登录
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String access_token = httpServletRequest.getHeader("access-token").substring(7);
log.info("判断用户是否想要登录x:{}",access_token);
JwtToken token = new JwtToken(access_token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
isAccessAllowed,验证权限,登录成功即表示拥有访问系统资源的权限,具体的资源权限会在shiro中进行鉴权。
/**
* 这里我们详细说明下为什么最终返回的都是true,即允许访问
* 例如我们提供一个地址 GET /article
* 登入用户和游客看到的内容是不同的
* 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
* 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
* 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
* 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
HttpServletResponse httpServletResponse =(HttpServletResponse)response;
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
HttpServletResponseUtil.printJson(httpServletResponse,new JsonResult(ResultCode.PERMISSION_TOKEN_INVALID));
return false;
}
}else{
return false;
}
}
preHandle提供跨域支持
/**
* 对跨域提供支持
*/
@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);
}
3. 重写realm类MyRealm并创建Shiro的配置类ShiroConfig
MyRealm中主要实现了两个方法doGetAuthenticationInfo、doGetAuthorizationInfo。
doGetAuthenticationInfo验证用户身份的真实性及token的有效性
/**
* 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
System.out.println("————身份认证方法————");
String token = (String) authenticationToken.getCredentials();
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
// 验证token的有效性,如果无效会抛出异常
try{
JwtTokenUtil.parseJWT(token,audience.getBase64Secret());
if(JwtTokenUtil.isExpiration(token,audience.getBase64Secret())){
HttpServletResponseUtil.printJson(response,new JsonResult(ResultCode.PERMISSION_TOKEN_EXPIRED));
}
}catch (Exception e){
HttpServletResponseUtil.printJson(response,new JsonResult(ResultCode.PERMISSION_TOKEN_INVALID));
}
return new SimpleAuthenticationInfo(token, token, "my_realm");
}
doGetAuthorizationInfo,对于需要验证的资源进行权限验证
/**
* 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("########################权限认证############################");
String userId = JwtTokenUtil.getUserId(principals.toString(), audience.getBase64Secret());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("/user/list");
return info;
}
ShiroConfig主要就是一些shiro的相关配置,这里面和JWT相关的配置就是禁用session、引入JwtFilter
package com.tdrc.common.core.shiro;
import org.apache.shiro.mgt.DefaultSecurityManager;
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.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author dpf
* @version 1.0
* @date 2020-5-22 15:01
* @instruction ...
*/
@Configuration
public class ShiroConfig {
private Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultSecurityManager securityManager) {
logger.info("######################Shiro 启动#######################################");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap<>();
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setUnauthorizedUrl("/401");
/**
* 自定义url规则
* http://shiro.apache.org/web.html#urls-
*/
Map filterRuleMap = new LinkedHashMap();
// 所有的请求通过我们自己的JWT filter
filterRuleMap.put("/401", "anon");
filterRuleMap.put("/404", "anon");
filterRuleMap.put("/*.png", "anon");
filterRuleMap.put("/user/login", "anon");
filterRuleMap.put("/article/**", "anon");
filterRuleMap.put("/**", "jwt");
// 访问401和404页面不通过我们的Filter
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilterFactoryBean;
}
@Bean(name = "securityManager")
public DefaultSecurityManager getDefaultSecurityManager(@Qualifier("myRelam") MyRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
/*
* 关闭shiro自带的session
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean(name="myRelam")
public MyRealm getMyRealm() {
return new MyRealm();
}
/**
* 下面的代码是添加注解支持
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
最后就是JWT的token的时效性,问题就是每次登陆都会创建一个新的token,在旧的token还没有过期之前都是可以使用的,还有就是集群或者分布式应用中无法保证token的唯一性,这里可以用引入缓存来解决这个问题。