SpringBoot+Shiro+JWT
一、Shiro
1、什么是shiro?
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的
API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Apache Shiro 的首要目标是易于使用和理解。安全有时候是很复杂的,甚至是痛苦的,但它没有必要这样。框架
应该尽可能掩盖复杂的地方,露出一个干净而直观的 API,来简化开发人员在使他们的应用程序安全上的努力。
2、shiro能做什么?
- 验证用户来核实他们的身份。
- 对用户执行访问控制。
- 判断用户是否被分配了一个确定的安全角色。
- 判断用户是否被允许做某事。
- 在任何环境下使用 Session API,即使没有 Web 或 EJB 容器。
- 在身份验证,访问控制期间或在会话的生命周期,对事件作出反应。
- 聚集一个或多个用户安全数据的数据源,并作为一个单一的复合用户“视图”。
- 启用单点登录(SSO)功能。
- 为没有关联到登录的用户启用"Remember Me"服务。
3、shiro有哪些功能模块?
Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。
-
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话
中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
"Run As":一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
"Remember Me":记住我。
4、shiro的内部结构是什么样的?
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
-
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实
现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所以,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的
Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
5、搭建一个shiro应用需要做什么?
1、应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
2、我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判
断。
二、JWT
1、什么是JWT?
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).定义了一种简洁的,自包含的方法用于通信双方之间以JSON对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
2、JWT的请求流程?
- 用户使用账号和密码发出post请求;
- 服务器使用私钥创建一个jwt;
- 服务器返回这个jwt给浏览器;
- 浏览器将该jwt串在请求头中像服务器发送请求;
- 服务器验证该jwt;
- 返回响应的资源给浏览器。
3、JWT适合的场景?
身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
4、JWT有哪些优点?
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
5、JWT的结构?
Jwt包含三部分
1、Header 头部
JWT的头部承载两部分信息:token类型和采用的加密算法。
{
"alg": "HS256",
"typ": "JWT"
}
声明加密的算法:通常直接使用 HMAC SHA256
声明类型:这里是jwt
加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。
- MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。不管文件多大,经过MD5后都能生成唯一的MD5值
- SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5
- HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
2、Payload 负载
载荷就是存放有效信息的地方。
有效信息包含三个部分
1.标准中注册的声明
建议但不强制使用:
- iss: jwt签发者
- sub: 面向的用户(jwt所面向的用户)
- aud: 接收jwt的一方
- exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
2.公共的声明
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
3.私有的声明
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
public static String createToken(String msg) {
try {
final Algorithm signer = Algorithm.HMAC256("私钥");
return JWT.create()
.withIssuer("jwt签发者")
.withSubject("面向的用户(jwt所面向的用户)")
.withAudience("接收jwt的一方")
.withClaim("放置JWT中的数据", msg)
.withExpiresAt(new Date())
.withNotBefore(new Date())
.withIssuedAt(new Date())
.withJWTId("jwtID")
.sign(signer);
} catch (Exception e) {
logger.warn("生成JWT失败!{}", e.getMessage());
return null;
}
}
3、Signature 签名/签证
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret(私钥)
这个部分需要base64加密后的header和base64加密后的payload使用,连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。
三、SpringBoot+Shiro+JWT Demo
新建一个SpringBoot项目
1、引入依赖
com.auth0
java-jwt
3.4.1
org.apache.shiro
shiro-spring
1.7.1
2、JWT Filter
封装一个返回体:
/**
* REST 服务标准 返回类型
* @author Administrator
*
*/
public class Result {
public Result(int code){
setCode(code);
}
/**
* 代码
*/
private int code;
private int status;
private String serverTime =new Date();
private String requestId;
private String exception;
/**
* 错误信息(提交失败)
*/
private BizError error;
private T data;
/**
* 消息信息
*/
private String message;
/**
* 校验参数错误信息
*/
private List invalidArgs;
}
JWT Filter:
/**
* JWT认证过滤
*
* @Author liao xiangdong
* @Date 2021/8/4 14:31
*/
@Component
public class JWTFilter extends BasicHttpAuthenticationFilter {
private static final Logger logger = LoggerFactory.getLogger(JWTFilter.class);
/**
* 判断是否已认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = SecurityUtils.getSubject();
return null != subject && subject.isAuthenticated();
}
/**
* 认证检查未通过执行该方法
*
* @param request
* @param response
* @return
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
//1.检查请求头中是否含有token
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
//2.如果客户端没有携带token,拦下请求
if (null==token || "".equals(token)) {
responseTokenError(response, "token无效,您无权访问该接口");
return false;
}
//3.如果有,对token进行验证
JWTToken jwtToken = new JWTToken(token);
try {
SecurityUtils.getSubject().login(jwtToken);
} catch (AuthenticationException e) {
logger.info("token验证失败!{}", e.getMessage());
responseTokenError(response, e.getMessage());
return false;
}
return true;
}
/**
* 对跨域提供支持
*
* @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);
}
/**
* 返回token认证错误
*
* @param response
* @param msg
*/
private void responseTokenError(ServletResponse response, String msg) {
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
try (PrintWriter out = httpServletResponse.getWriter()) {
ObjectMapper objectMapper = new ObjectMapper();
Result result = new Result<>(5001);
result.setMessage(msg);
String content = objectMapper.writeValueAsString(result);
out.append(content);
} catch (IOException e) {
logger.error("返回token认证错误信息失败!{}", e.getMessage());
}
}
}
3、JWT
/**
* JWT Token
*
* @Author liao xiangdong
* @Date 2021/8/5 10:12
*/
public class JWTToken implements AuthenticationToken {
/**
* 密钥
*/
private String token;
public JWTToken(String token) {
this.token = token;
}
public String getToken() {
return this.token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
4、JWT Util
public class JWTUtil {
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
/**
* 加密生成token
* @param username 用户名
* @param role 用户角色
* @return token
*/
public static String createToken(String msg) {
try {
final Algorithm signer = Algorithm.HMAC256("私钥");
return JWT.create()
.withIssuer("jwt签发者")
.withSubject("面向的用户(jwt所面向的用户)")
.withAudience("接收jwt的一方")
.withClaim("放置JWT中的数据", msg)
.withExpiresAt(new Date())
.withNotBefore(new Date())
.withIssuedAt(new Date())
.withJWTId("jwtID")
.sign(signer);
} catch (Exception e) {
logger.warn("生成JWT失败!{}", e.getMessage());
return null;
}
}
/**
* 解析验证token
* @param token 加密后的token字符串
* @return 验证是否通过
*/
public static Boolean verifyToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256("私钥");
JWTVerifier verifier = JWT.require(algorithm).build();
verifier.verify(token);
return true;
} catch (Exception e) {
logger.warn("校验JWT失败!{}",e.getMessage());
}
return false;
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @param token token
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
logger.warn("解密JWT失败!{}",e.getMessage());
return null;
}
}
}
5、自定义Realm
/**
* 自定义realm
*
* @Author liao xiangdong
* @Date 2021/8/4 14:40
*/
public class UserRealm extends AuthorizingRealm {
private UserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
/**
* 鉴权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
User activeUser = (User) SecurityUtils.getSubject().getPrincipal();
authorizationInfo.addStringPermission(activeUser.getRole());
return authorizationInfo;
}
/**
* 根据token进行登陆验证
*
* @param auth
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
//解决空对象
preHandleNull();
String token = (String) auth.getCredentials();
//获得token中的username
String username = JWTUtil.getUsername(token);
if (username == null) {
throw new AuthenticationException("token错误,请重新登入!");
}
User user = userService.findUserByName(username);
if (user == null) {
throw new AccountException("账号不存在!");
}
//判断redis中存入的token是否过期
if (userService.isExpire(username, token)) {
throw new AuthenticationException("token过期,请重新登入!");
}
if (!JWTUtil.verifyToken(token)) {
throw new AuthenticationException("token错误,请重新登入!");
}
if (!"0".equals(sqzlUser.getStatus())) {
throw new LockedAccountException("账号已被锁定!");
}
return new SimpleAuthenticationInfo(user, token, getName());
}
/**
* Shiro在Spring自动装配bean之前实例化
* 相关的Bean都被初始化完成且没有被代理,使用动态获取代理对象即可解决
*/
protected void preHandleNull() {
if (null == userService) {
userService = SpringContextUtils.getBean(UserService.class);
}
}
}
SpringContextUtils:
/**
* SpringContextUtils工具类
* @Author liao xiangdong
* @Date 2021/8/10 10:50
*/
public class SpringContextUtils{
private static ApplicationContext applicationContext;
public static void setApplicationContext(ApplicationContext context) {
applicationContext = context;
}
public static T getBean(Class requiredType) {
return applicationContext.getBean(requiredType);
}
}
MainApp:
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MainApp.class);
ConfigurableApplicationContext ctx=application.run(args);
SpringContextUtils.setApplicationContext(ctx);
}
6、ShiroConfig
/**
* Shiro配置类
*
* @Author liao xiangdong
* @Date 2021/8/4 14:25
*/
@Configuration
public class ShiroConfig {
@Bean
public DefaultWebSecurityManager getManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//使用自定义realm
manager.setRealm(new UserRealm());
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean
public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//添加自己的过滤器并且取名为jwt
Map filterMap = new HashMap<>();
filterMap.put("jwt", new JWTFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
//自定义url规则
Map filterRuleMap = new HashMap<>();
filterRuleMap.put("/api/login/**", "anon");
filterRuleMap.put("/api/register/**", "anon");
//开放API文档接口
filterRuleMap.put("/swagger-ui.html", "anon");
filterRuleMap.put("/swagger-resources/**", "anon");
filterRuleMap.put("/api/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 添加注解支持
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
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;
}
}
7、解决JWT的过期与续期问题
JWT中可以指定过期时间,当前时间大于JWT中的过期时间时,JWT失效。如果指定过期时间,会导致无法续期,因为采用的方式为登陆成功时将用户名与生成的token作为键值对存入redis中,设置过期时间半小时,每次请求时刷新过期时间。从而控制JWT的过期与续期。
8、单元测试中模拟请求走自定义的Filter的问题
可以在单元测试的基础类中模拟一个filter,这样测试时不会调动远程接口
/**
* 使用此注解注入的类,表明类中的所有方法都使用自定义返回的值
* 这样在测试的时候就不会真的去调用远程接口,而是返回一个我们预设的值,默认返回null
*/
@MockBean
private JWTFilter jwtFilter;
@BeforeEach
public void setupMockMvc(){
//初始化MockMvc对象
mvc = MockMvcBuilders.webAppContextSetup(context).addFilters(jwtFilter).build();
}