一、什么是Shiro
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
三个核心组件:Subject, SecurityManager 和 Realms
三大核心组件:
Subject:主体
主体,代表了当前的“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是主体,如第三方进程、网络爬虫、机器人等,Subject是一个抽象概念,所有的Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager,可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager:安全管理器
安全管理器,即所有与安全有关的操作都会与SecurityManager进行交互,是Shiro框架的核心,管理所有的Subject,类似于Spring
MVC的前端控制器DispatcherServlet;
Realm:域
Shiro从Realm中获取安全数据(比如用户、角色、权限),SecurityManager要验证用户身份,需要从Realm中获取相应的用户进行比较确定用户是否合法;验证用户角色/权限也需要从Realm获得相应数据进行比较,类似于DataSource,安全数据源;它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。
需要注意的是:Shiro本身不提供维护用户、权限,而是通过Realm让开发人员自己注入到SecurityManager,从而让SecurityManager能得到合法的用户以及权限进行判断;
二、项目代码实战
1.项目依赖
只说明Shiro和JWT的依赖SpringBoot的自己配置
<!--整合Shiro安全框架-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
<!--集成jwt实现token认证-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
2.JWT工具类编写
既然要使用JWT第一步我们直接先准备JWT工具类编写
博主只是根据自己的需求去传入的参数,如果项目不一样,自行更改就好了
JWT就不用多说了吧
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import java.io.UnsupportedEncodingException;
import java.util.Calendar;
import java.util.Date;
/**
* @program:
* @description:
* @author: sun jingchun
* @create: 2021-10-26 16:47
**/
public class JwtUtils {
/**
* 密钥
* */
private static final String SECRET = "889556654";
public static String createToken(String username, String password) throws UnsupportedEncodingException {
//设置token时间 三小时
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.HOUR, 3);
Date date = nowTime.getTime();
//密文生成
String token = JWT.create()
.withClaim("username", username)
.withClaim("password", password)
.withExpiresAt(date)
.withIssuedAt(new Date())
.sign(Algorithm.HMAC256(SECRET));
return token;
}
/**
* 验证token的有效性
* */
public static boolean verify(String token) {
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
verifier.verify(token);
return true;
} catch (UnsupportedEncodingException e) {
return false;
}
}
/**
* 获取token列名
* **/
/**
* 通过载荷名字获取载荷的值
* */
public static String getClaim(String token, String name){
String claim = null;
try {
claim = JWT.decode(token).getClaim(name).asString();
}catch (Exception e) {
return "getClaimFalse";
}
return claim;
}
3.编写一个JwtToken.class
编写一个JwtToken.c类 继承 AuthenticationToken 之后会用到
import org.apache.shiro.authc.AuthenticationToken;
/**
* @program:
* @description:
* @author: sun jingchun
* @create: 2021-10-26 17:44
**/
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;
}
}
4.编写Realm类
package com.dfinfo.smalabelbackend.filter;
import com.dfinfo.smalabelbackend.common.util.JwtUtils;
import com.dfinfo.smalabelbackend.service.UserService;
import org.apache.commons.lang.StringUtils;
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.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @program: smalabel-backend
* @description:
* @author: 孙靖淳
* @create: 2021-10-26 17:52
**/
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
//
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 只有当检测用户需要权限或者需要判定角色的时候才会走
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("MyRealm doGetAuthorizationInfo() 方法授权 ");
String token = principalCollection.toString();
String username = JwtUtils.getClaim(token,"username");
if (StringUtils.isBlank(username)) {
throw new AuthenticationException("token认证失败");
}
//查询当前
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//其实这里应该是查询当前用户的角色或者权限去的,意思就是将当前用户设置一个svip和vip角色
//权限设置一级权限和耳机权限 正常来说应该是去读取数据库只添加当前用户的角色权限的
info.addRole("vip");
info.addRole("svip");
info.addStringPermission("一级权限");
info.addStringPermission("二级权限");
System.out.println("方法结束咯-------》》》");
return info;
}
/**
* 默认使用此方法进行用户名正确与否验证, 如果没有权限注解的话就不会去走上面的方法只会走这个方法
* 其实就是 过滤器传过来的token 然后进行 验证 authenticationToken.toString() 获取的就是
* 你的token字符串,然后你在里面做逻辑验证就好了,没通过的话直接抛出异常就可以了
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证-----------》》》》");
System.out.println("1.toString ------>>> " + authenticationToken.toString());
System.out.println("2.getCredentials ------>>> " + authenticationToken.getCredentials().toString());
System.out.println("3. -------------》》 " +authenticationToken.getPrincipal().toString());
String jwt = (String) authenticationToken.getCredentials();
// if (!JwtUtils.verify(jwt)) {
// throw new AuthenticationException("Token认证失败");
// }
return new SimpleAuthenticationInfo(jwt, jwt, "MyRealm");
}
}
5.写JWTFiler(JWT过滤器)
JWT过滤器有两种写法我只写其中一种,继承BasicHttpAuthenticationFilter
另外一种方法请自行百度
详情解释请看代码
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @program: smalabel-backend
* @description:
* @author: sun jingchun
* @create: 2021-10-26 17:00
**/
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 拦截器的前置 最先执行的 这里只做了一个跨域设置
* */
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
System.out.println("JwtFilter -----> preHandle() 方法执行");
HttpServletRequest req= (HttpServletRequest) request;
HttpServletResponse res= (HttpServletResponse) response;
res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));
res.setHeader("Access-control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");
res.setHeader("Access-control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
res.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
/**
* preHandle 执行完之后会执行这个方法
* 再这个方法中 我们根据条件判断去去执行isLoginAttempt和executeLogin方法
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
System.out.println("JwtFilter -----> isAccessAllowed() 方法执行");
/**
* 先去调用 isLoginAttempt方法 字面意思就是是否尝试登陆 如果为true
* 执行executeLogin方法
*/
if (isLoginAttempt(request,response)) {
executeLogin(request, response);
return true;
}
return true;
}
/**
* 这里我们只是简单去做一个判断请求头中的token信息是否为空
* 如果没有我们想要的请求头信息则直接返回false
* */
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
System.out.println( "JwtFilter -----> isLoginAttempt() 方法执行");
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
return token != null;
}
/**
* 执行登陆
* 因为已经判断token不为空了,所以直接执行登陆逻辑
* 讲token放入JwtToken类中去
* 然后getSubject方法是调用到了MyRealm的 执行方法 因为上面我是抛错的所有最后做个异常捕获就好了
* */
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
System.out.println("JwtFilter -----> executeLogin() 方法执行");
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
JwtToken jwtToken = new JwtToken(token);
//然后交给自定义的realm对象去登陆, 如果错误他会抛出异常并且捕获
System.out.println("-----执行登陆开始-----");
getSubject(request, response).login(jwtToken);
System.out.println("-----执行登陆结束----- 未抛出异常");
return true;
}
}
6.配置ShiroConfig将配置注入到容器中
import com.dfinfo.smalabelbackend.filter.JwtFilter;
import com.dfinfo.smalabelbackend.filter.MyRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.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 javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @program: smalabel-backend
* @description:
* @author: 孙靖淳
* @create: 2021-10-26 16:52
**/
@Configuration
public class ShiroConfig {
/**
* 注入 securityManager
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager(MyRealm myRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置自定义 realm.
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
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//添加自己的过滤器 并且取名为filter
Map<String, Filter> filterMap = new LinkedHashMap<>();
//设置自定义的JWT过滤器
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setSecurityManager(securityManager);
//设置无权限跳转的url 权限验证如果没权限跳转
factoryBean.setUnauthorizedUrl("/noRole");
Map<String, String> filterRuleMap = new HashMap<>();
//设置需要通过过滤器的请求 /**意思是 所有请求接口都通过自定义的jwt过滤器
//anon的意思是 不需要走这一套逻辑 自己配置就好了
filterRuleMap.put("/**", "jwt");
filterRuleMap.put("/login","anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
/**
* 添加注解支持
* */
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
7.配置权限校验或者角色校验
在你的接口上添加注解就好了 上面是角色注解 下面是权限 拥有这些的才能访问接口
前面也说了 myrealm类中 的两个方法 如果不加注解的话 权限方法是根本不会执行的 可以自己做测试
8.来看看结果
1.登陆接口直接放行的
看 并没有输出JWT过滤器中的内容 所有能够用理解anon的作用了吧
2.没放行,也没权限校验的接口
登陆接口那个字符串是上面输出的 不管
这里很容易就能理解了执行的顺序
jwt过滤器里面的先执行 然后按方法执行
因为没有判断权限注解所以只吊了认证方法 认证没错误 然后到接口
3. 有权限角色认证
权限或者角色错误时
看图 :我设置的权限和角色和接口的并不一致 ,结果报错,然后再看,使用了权限注解后授权方法就执行了,这就是整个执行顺序
信息补充的话去这个博客查看
Springboot + Shiro