Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
Shiro最重要的三大概念:
-
Subject:用户(当然并不一定是用户,也可以指和当前应用交互的任何对象),我们在进行授权鉴权的所有操作都是围绕Subject(用户)展开的,在当前应用的任何地方都可以通过
SecurityUtils
的静态方法getSubject()
轻松的拿到当前认证(登录)的用户。 -
SecurityManager:安全管理器,Shiro中最核心的组件,它管理着当前应用中所有的安全操作,包括Subject(用户),我们围绕Subject展开的所有操作都需要与SecurityManager进行交互。可以理解为SpringMVC中的前端控制器。
-
Realms:字面意思为领域,Shiro在进行权限操作时,需要从Realms中获取安全数据,也就是用户以及用户的角色和权限。配置Shiro,我们至少需要配置一个Realms,用于用户的认证和授权。通常我们的角色及权限信息都是存放在数据库中,所以Realms也可以算是一个权限相关的Dao层,SecurityManager在进行鉴权时会从Realms中获取权限信息。
1.环境搭建
1 引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
2 创建shiroConfig类
package com.hqyj.j220701.springboot.springbootdemo01.shiro;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
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.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;
@Configuration
public class ShiroConfig {
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
// 把安全管理器添加到filter中
factoryBean.setSecurityManager(securityManager());
factoryBean.setLoginUrl("/api/user/login");
// 配置自定义过滤器
Map<String, Filter> filers = new HashMap<String, Filter>();
// 使用new方式,不要把JwtFilter注入到spring容器中
filers.put("jwt",new JwtFilter());
factoryBean.setFilters(filers);
Map<String, String> filterResultMap = new HashMap<>();
filterResultMap.put("/api/user/login", "anon");
filterResultMap.put("/api/auth/401", "anon");
filterResultMap.put("/**", "authc");
factoryBean.setFilterChainDefinitionMap(filterResultMap);
factoryBean.setUnauthorizedUrl("/api/auth/401");
return factoryBean;
}
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
// 打开注解
proxyCreator.setProxyTargetClass(true);
// 开启前缀匹配
proxyCreator.setUsePrefix(true);
proxyCreator.setAdvisorBeanNamePrefix("authorizationAttributeSourceAdvisor");
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
}
注意:上面Filter的配置顺序不能随便打乱,过滤器是按照我们配置的顺序来执行的。范围大的过滤器要放在后面,/**
这条如果放在前面,那么一来就匹配上了,就不会继续再往后走了。
Shiro提供了很多内置的过滤器,我们最常用的就是第一个和第二个。如果对其效果不满意,我们还可以自定义过滤器实现权限控制。
anthc过滤拦截过程
UserRealm为自定义Realm,需要创建该类,该类继承AuthorizingRealm,实现doGetAuthorizationInfo和doGetAuthenticationInfo方法。
doGetAuthenticationInfo():认证。相当于登录,只有通过登录了,才能进行后面授权的操作。一些只需要登录权限的操作,在登录成功后就可以访问了,比如上一步中配置的authc
过滤器就是只需要登录权限的。
doGetAuthorizationInfo():授权。认证过后,仅仅拥有登录权限,更多细粒度的权限控制,比如菜单权限,按钮权限,甚至方法调用权限等,都可以通过授权轻松实现。在这个方法里,我们可以拿到当前登录的用户,再根据实际业务赋予用户部分或全部权限,当然这里也可以赋予用户某些角色,后面也可以根据角色鉴权。下方的演示代码仅添加了权限,赋予角色可以调用addRoles()
或者setRoles()
方法,传入角色集合。
创建UserRealm类
package com.hqyj.j220701.springboot.springbootdemo01.shiro;
import com.hqyj.j220701.springboot.springbootdemo01.common.component.JwtComponent;
import com.hqyj.j220701.springboot.springbootdemo01.user.dto.UserInfo;
import com.hqyj.j220701.springboot.springbootdemo01.user.dto.UserPerms;
import com.hqyj.j220701.springboot.springbootdemo01.user.entity.User;
import com.hqyj.j220701.springboot.springbootdemo01.user.service.UserService;
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.authz.UnauthenticatedException;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class UserRealm extends AuthorizingRealm {
@Autowired
private JwtComponent jwtComponent;
@Autowired
private UserService userService;
/*
* 授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User)principalCollection.getPrimaryPrincipal();
Integer id = user.getId();
List<UserPerms> userPermsList = userService.getUserPermsById(id);
if (CollectionUtils.isEmpty(userPermsList)){
throw new UnauthenticatedException();
}
Set<String> roles = new HashSet<>();
Set<String> perms = new HashSet<>();
for (UserPerms userPerms : userPermsList) {
roles.add(userPerms.getRoleCode());
perms.add(userPerms.getPermsCode());
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(perms);
return info;
}
/*
* 认证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = authenticationToken.getPrincipal().toString();
String userName = jwtComponent.getUserName(token);
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
User user = userService.getUserInfo(userInfo);
if (Objects.isNull(user)){
throw new UnauthenticatedException();
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,token,this.getName());
return info;
}
/*
* 支持JWTToken
* */
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
}
用户授权常用有两种方式:
第一种在shiroConfig类中的ShiroFilterFactoryBean对象中配置,例如配置角色:
filterResultMap.put("/api/user/info","roles[admin]");
该方式需要手动配置,容易出错。
第二种使用注解:
@RequiresRoles("admin")
@RequiresPermissions(value = {"user:info"})
该方式相对较灵活,需要事先在shiroConfig类中配置AdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor,开启权限注解。
注解式权限配置:
注解式的权限控制需要在ShiroCofig类中配置两个Bean:DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor。
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
// 打开注解
proxyCreator.setProxyTargetClass(true);
// 开启前缀匹配
proxyCreator.setUsePrefix(true);
proxyCreator.setAdvisorBeanNamePrefix("authorizationAttributeSourceAdvisor");
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
AdvisorAutoProxyCreator:代理生成器,需要借助SpringAOP来扫描@RequiresRoles和@RequiresPermissions等注解,生成代理类实现功能增强,从而实现权限控制。
AuthorizationAttributeSourceAdvisor:需要配合AdvisorAutoProxyCreator一起使用,否则权限注解无效。
Advisor 中包含 PointCut 和 Advice 。其中 PointCut 代表切点,代表要增强的点,Advice 中编写了具体的增强实现。
Spring在启动时会通过自动代理创建器去扫描所有的Advisor 实现类,并在加载每个Bean的时候判断Advisor 是否适用于当前Bean,如果适用,则会通过Advice 来创建该Bean的增强代理。
2.基于JWT的认证
基于jwt的认证一般应用在前后端分离的项目中,使用token认证,具体原理很简单,客户端每次请求服务器的时候都会带上token密钥,服务器识别认证token后,即可识别身份并响应数据。
JWT(Json Web Token) 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。
JWT标准的Tokens由三部分组成
-
header:包含token的类型和加密算法
-
payload:包含token的内容
-
signature:通过密钥将前两者加密得到最终的token
JWT使用流程:
-
首先,前端通过 Web 表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个 HTTP POST 请求。
-
后端核对用户名和密码成功后,将用户信息作为 JWT Payload(负载),将其与头部分别进行 Base64 编码拼接后签名,形成一个 JWT。形成的 JWT 就是一个形同 lll.zzz.xxx 的字符串。
-
后端将 JWT 字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在 localStorage 或 sessionStorage 上,退出登录时前端删除保存的 JWT 即可。
-
前端在每次请求时将 JWT 放入 HTTP Header 中的 Authorization 位。
-
后端检查是否存在,如存在验证 JWT 的有效性。例如,检查签名是否正确;检查 Token 是否过期。
-
验证通过后后端使用 JWT 中包含的用户信息进行其他逻辑操作,返回相应结果。
1 配置
导入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.7.0</version>
</dependency>
创建JWT工具类
在jwt工具类中有生成token的方法、校验token是否有效是否过期的方法、以及通过token获取解析值的方法。
package com.hqyj.j220701.springboot.springbootdemo01.common.component;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Calendar;
import java.util.Date;
@Component
public class JwtComponent {
@Value("${system.expireTime}")
private Integer expireTime;
/*
* 生成JWTToken
* */
public String sign(String userName,String secret){
Algorithm algorithm = Algorithm.HMAC256(userName + secret);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND,expireTime);
Date time = calendar.getTime();
return JWT.create().withClaim("userName", userName).withExpiresAt(time).sign(algorithm);
}
/*
* 从JWTToken中获取用户名
* */
public String getUserName(String token){
return JWT.decode(token).getClaim("userName").asString();
}
/*
* 从JWTToken中获取过期时间
* */
public Date getExpireTime(String token){
return JWT.decode(token).getExpiresAt();
}
}
2 shiro过滤器
前端页面接收token后,认为登录成功,将token保存在本地,在发送其他请求时在请求头信息中添加Authorization,将token存入Authorization。
而前后端分离后,前端页面向后台发送请求时是跨域访问,跨域时会先发送一个OPTIONS(预请求),这个预请求是不带Authorization Info的,此时shiro对OPTIONS不能认证成功,导致请求失败。
后台需要创建一个过滤器,在接收到OPTIONS请求时使其通过,在对真正的请求进行验证。步骤如下:
在shiroConfig类中关闭shiro自带的session
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
创建shiro过滤器
package com.hqyj.j220701.springboot.springbootdemo01.shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
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;
import java.io.IOException;
/*
* 方法执行顺序:
* preHandle > isAccessAllowed > isLoginAttempt > executeLogin > onAccessDenied
* */
public class JwtFilter extends BasicHttpAuthenticationFilter {
/*
* 允许shiro请求跨域
* 放行OPTIONS请求
* */
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
// 设置允许跨域的方法
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,PUT,DELETE");
res.setHeader("Access-Control-Allow-Headers",
req.getHeader("Access-Control-Request-Headers"));
// 放行OPTIONS请求
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
res.setStatus(HttpStatus.OK.value());
return true;
}
return super.preHandle(request, response);
}
/*
* 判断请求中是否带有Authorization的token(isLoginAttempt)
* 如果存在,实现登录认证(executeLogin)
* */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request,response)){
try {
executeLogin(request,response);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
// 根据业务需求来设置是否放行,比如允许匿名访问的时候可以return true
return true;
}
/*
* 获取前端请求,验证请求头信息中是否有Authorization的token
* */
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest)request;
String token = req.getHeader("Authorization");
if (StringUtils.isEmpty(token)){
return false;
}
else {
return true;
}
}
/*
*登录认证
* 通过subject.login方法认证
* */
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest)request;
String token = req.getHeader("Authorization");
// 自定义token
JWTToken jwtToken = new JWTToken(token);
Subject subject = SecurityUtils.getSubject();
subject.login(jwtToken);
return true;
}
/*
* 如果验证不通过(isAccessAllowed返回false,或者是有异常抛出)则执行该方法
* 调用异常接口,向前端返回异常提示
* */
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
response401(request,response);
return false;
}
private void response401(ServletRequest request, ServletResponse response) {
HttpServletResponse res = (HttpServletResponse) response;
try {
res.sendRedirect("/api/user/401");
} catch (IOException e) {
e.printStackTrace();
}
}
}
preHandler:重写该方法主要是过滤掉跨域访问时发送的OPTIONS请求,使该请求能够允许跨域。
isAccessAllowed:该方法中首先判断用户是否尝试登录(携带token),是则执行executeLogin登录,否则返回true,让shiro验证继续执行,而不跳转到onAccessDenied方法。
onAccessDenied:当isAccessAllowed返回false时跳转到该方法,重写该方法为了避免循环调用doGetAuthenticationInfo方法。
isLoginAttempt:判断请求头信息中是否包含Authorization的token。
executeLogin:执行subject.login方法,调用userRealm中的doGetAuthenticationInfo方法。
加入过滤器后整个前后端分离的用户登录认证流程
3.全局异常处理
SpringBoot中,@ControllerAdvice 是一个controller增强器,可以对controller中使用到@RequestMapping注解的以下方法做异常逻辑处理,使用该注解表示开启了全局异常的捕获,我们只需在自定义一个方法使用@ExceptionHandler注解然后定义捕获异常的类型即可对这些捕获的异常进行统一的处理。
@ControllerAdvice
public class BootControllerAdvice {
@ExceptionHandler(Exception.class)
@ResponseBody
public Result<?> handleError() {
return new Result<>().error(500, "用户操作失败");
}
@ExceptionHandler(value = UnauthorizedException.class)
@ResponseBody
public Result<?> handleError(UnauthorizedException e) {
e.printStackTrace();
return new Result<>().error(HttpStatus.UNAUTHORIZED.value(), "用户无权限操作");
}
}
@ControllerAdvice:在该类中,可以根据不同的异常类型定义对应多个方法,不同的方法处理不同的异常;也可以在一个方法中处理所有的异常信息。
@ExceptionHandler 注解用来指明异常的处理类型,例如当shiro抛出ShiroException异常时,则会通过handleError方法进行处理。