序言
本文讲解如何使用SpringBoot整合Shiro框架来实现认证及权限校验,但如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot整合Shiro框架的时候会联合JWT一起搭配使用。 
Shiro
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份 认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
Shiro架构图 
Shiro 核心组件
用户、角色、权限之间的关系
1、UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token。 2、SecurityManager,Shiro 的核心部分,负责安全认证和授权。 3、Suject,Shiro 的一个抽象概念,包含了用户信息。 4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。 5、AuthenticationInfo,用户的角色信息集合,认证时使用。 6、AuthorzationInfo,角色的权限信息集合,授权时使用。 7、DefaultWebSecurityManager,安全管理器,开发者自定义的Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。 8、ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。
JWT
JWT(JSON WEB TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。
JWT的构成 JWT由三部分构成:Header(头部)、Payload(载荷)和Signature(签名)。 
1.Header(头) 作用:记录令牌类型、签名算法等 例如:{“alg":"HS256","type","JWT}
2.Payload(有效载荷)作用:携带一些用户信息 例如{"userId":"1","username":"mayikt"}
3.Signature(签名)作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串
项目环境
- Shiro:1.4.1
- SpringBoot:2.5.6
- JDK:1.8
搭建项目
pom依赖
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.15</version>
</dependency>
</dependencies>
JWTUtil
public class JWTUtils {
private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;
public static boolean verify(String token, String username, String password) {
try {
Algorithm algorithm = Algorithm.HMAC256(password);
JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e) {
return false;
}
}
public static String sign(String username, String password) {
try {
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(password);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (UnsupportedEncodingException e) {
return null;
}
}
public static String getUsername(String token) {
if (token == null || "".equals(token)) {
return null;
}
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
return null;
}
}
}
JWTToken
JWTToken是定义的一个Token类,继承了AuthenticationToken类,实现getPrincipal和getCredentials方法,(这两个方法本来是用于获取token中的信息,和识别token的,但JWTUtils已经为我们提供了这样的方法,所以这两个方法对于JWTToken没有意义)。用于将客户端传来的Token进行封装,便于Realm识别Token类型,进行认证和授权。
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;
}
}
JWTFilter过滤器
因为 JWT 的整合,我们需要?定义??的过滤器 JWTFilter,JWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原?法进?了重写。
public class JWTFilter extends BasicHttpAuthenticationFilter {
private static String LOGIN_SIGN = "Authorization";
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
if (isLoginAttempt(request, response)) {
try {
executeLogin(request, response);
} catch (Exception e) {
if (e instanceof AuthorizationException) {
throw new AuthorizationException("访问资源权限不足!");
} else {
throw new AuthenticationException("token 异常 认证失败");
}
}
}
return true;
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String authorization = req.getHeader(LOGIN_SIGN);
return authorization != null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
String header = req.getHeader(LOGIN_SIGN);
JWTToken token = new JWTToken(header);
getSubject(request, response).login(token);
return true;
}
}
自定义ShiroRealm
自定义的Realm对象,该对象继承于AuthorizingRealm,实现了Shiro具体认证和授权的方法。
doGetAuthenticationInfo方法用于->认证:校验帐号和密码doGetAuthorizationInfo方法用于->授权:授予角色和权限
另外需要注意: 必须要重写supports方法,因为是自己定义的Token,shiro无法识别,需要修改Realm中的supports方法,使 shiro 支持自定义Token。
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private RoleService roleService;
@Autowired
private MenuService menuService;
@Autowired
private UserService userService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String token = (String) authenticationToken.getCredentials();
String username = JWTUtils.getUsername(token);
User user = userService.selectByUserName(username);
if (user != null) {
if (!JWTUtils.verify(token, username, user.getPassword())) {
throw new IncorrectCredentialsException();
}
return new SimpleAuthenticationInfo(token, token, getName());
} else {
throw new UnknownAccountException();
}
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String userName = JWTUtils.getUsername(principals.toString());
User user = userService.selectByUserName(userName);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if (user != null) {
List<Role> roles = roleService.selectRoleByUserId(user.getId());
for (Role role : roles) {
info.addRole(role.getRoleKey());
}
List<Menu> permissions = menuService.selectPermsByUserId(user.getId());
for (Menu permission : permissions) {
info.addStringPermission(permission.getPerms());
}
}
return info;
}
}
ShiroConfig
ShiroConfig用于进行Shiro的相关配置,主要包括ShiroFilterFactoryBean、DefaultWebSecurityManager和Realm的配置。
@Configuration
public class ShiroConfig {
@Bean(name = "lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean(name = "shiroRealm")
@DependsOn("lifecycleBeanPostProcessor")
public ShiroRealm shiroRealm(EhCacheManager cacheManager) {
ShiroRealm realm = new ShiroRealm();
realm.setCacheManager(cacheManager);
return realm;
}
@Bean(name = "ehCacheManager")
@DependsOn("lifecycleBeanPostProcessor")
public EhCacheManager ehCacheManager() {
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
return ehCacheManager;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setCacheManager(ehCacheManager());
DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(evaluator);
securityManager.setSubjectDAO(subjectDAO);
securityManager.setRealm(shiroRealm);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
factoryBean.setFilters(filters);
Map<String, String> filterChainDefinitionManager = new LinkedHashMap<>();
filterChainDefinitionManager.put("/**", "jwt");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
return factoryBean;
}
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
aASA.setSecurityManager(securityManager);
return aASA;
}
}
这里开启注解支持需要添加DefaultAdvisorAutoProxyCreator(可选) 和AuthorizationAttributeSourceAdvisor ,DefaultAdvisorAutoProxyCreator也可以选择不加,这里加是因为防止重复代理和可能引起代理出错的问题
认证和授权规则
认证过滤器
anon:无需认证。authc:必须认证。authcBasic:需要通过 HTTPBasic 认证。 授权过滤器perms:必须拥有某个权限才能访问。role:必须拥有某个角色才能访问。port:请求的端口必须是指定值才可以。rest:请求必须基于 RESTful,POST、PUT、GET、DELETE。ssl:必须是安全的 URL 请求,协议 HTTPS。
自定义异常处理
使用@RestControllerAdvice捕获Controller层抛出的异常。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.OK)
public Object errorHandler(Exception e, HttpServletRequest httpServletRequest) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("success", false);
if (e instanceof NoHandlerFoundException) {
jsonObject.put("code", 404);
jsonObject.put("msg", "找不到请求资源");
} else if (e instanceof MissingServletRequestParameterException) {
jsonObject.put("code", -200);
jsonObject.put("msg", "缺少参数");
} else if (e instanceof UnauthenticatedException) {
jsonObject.put("code", 401);
jsonObject.put("msg", "用户未登录,请登录");
} else if (e instanceof AuthorizationException) {
jsonObject.put("code", 402);
jsonObject.put("msg", "权限不足");
} else if (e instanceof AuthenticationException) {
jsonObject.put("code", 403);
jsonObject.put("msg", "帐号密码错误,请重新登录");
} else if (e instanceof MaxUploadSizeExceededException) {
jsonObject.put("code", 240);
jsonObject.put("msg", "文件上传超出大小限制");
} else if (e instanceof SQLException) {
jsonObject.put("code", 250);
jsonObject.put("msg", "数据库操作失败");
} else if (e instanceof SocketTimeoutException) {
jsonObject.put("code", 260);
jsonObject.put("msg", "服务连接超时");
} else if (e instanceof SocketException) {
jsonObject.put("code", 240);
jsonObject.put("msg", "服务连接失败");
} else if (e instanceof IOException) {
jsonObject.put("code", 500);
jsonObject.put("msg", "系统错误");
e.printStackTrace();
} else {
jsonObject.put("code", 500);
jsonObject.put("msg", "系统错误");
e.printStackTrace();
}
return jsonObject;
}
}
定义User
用户实体类
@Data
public class User {
private Integer id;
private String userName;
private String password;
private String salt;
}
定义Role
角色实体类
@Data
public class Role {
private Integer id;
private String roleKey;
}
Menu
菜单实体类
@Data
public class Menu {
private Integer id;
private String perms;
}
UserService
接口
public interface UserService {
User selectByUserName(String username);
}
实现类
@Service
public class UserServiceImpl implements UserService {
@Override
public User selectByUserName(String username) {
User user = new User();
user.setUserName(username);
user.setPassword("dc483e80a7a0bd9ef71d8cf973673924");
return user;
}
}
这里为了方便演示把密码写死了
RoleService
接口
public interface RoleService {
List<Role> selectRoleByUserId(Integer id);
}
实现类
@Service
public class RoleServiceImpl implements RoleService {
@Override
public List<Role> selectRoleByUserId(Integer id) {
List<Role> roles = new ArrayList<>();
Role admin = new Role();
admin.setRoleKey("admin");
roles.add(admin);
return roles;
}
}
这里为了方便演示把角色写死了
MenuService
接口
public interface MenuService {
List<Menu> selectPermsByUserId(Integer id);
}
实现类
@Service
public class MenuServiceImpl implements MenuService {
@Override
public List<Menu> selectPermsByUserId(Integer id) {
Menu saveUser = new Menu();
saveUser.setPerms("sys:user:save");
List<Menu> menus = new ArrayList<>();
menus.add(saveUser);
return menus;
}
}
这里为了方便演示把权限写死了
统一结果集工具
public class R extends HashMap<String, Object> {
public static final int SUCCESS_CODE = 200;
private R() {
}
public static R build(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R success() {
R r = new R();
r.put("code", 200);
r.put("msg", "success");
return r;
}
public static R success(String key, Object value) {
R r = R.success();
r.put(key, value);
return r;
}
public static R failure() {
R r = new R();
r.put("code", 500);
r.put("msg", "操作失败");
return r;
}
public static R failure(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public R add(String key, Object value) {
super.put(key, value);
return this;
}
public R delete(String key) {
if (key != null && (!"code".equals(key) || !"msg".equals(key))) {
this.remove(key);
}
return this;
}
public int getCode() {
return (int) this.get("code");
}
public void setCode(int code) {
this.put("code", code);
}
public String getMsg() {
return (String) this.get("msg");
}
public void setMsg(String msg) {
this.put("msg", msg);
}
}
ehcache.xml缓存配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="java.io.tmpdir/Tmp_EhCache"/>
<defaultCache
eternal="false"
maxElementsInMemory="1000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="600"
memoryStoreEvictionPolicy="LRU"/>
<cache name="user"
eternal="false"
maxElementsInMemory="10000"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
memoryStoreEvictionPolicy="LFU"/>
</ehcache>
配置详解
定义登录接口
@RestController
public class LoginController {
@Autowired
private UserService service;
@RequestMapping("/login")
public R login(String userName, String password) {
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return R.failure(590, "帐号或者密码不能为空");
}
User user = service.selectByUserName(userName);
if (user == null) {
return R.failure(590, "帐号不存在");
}
if (!encrypt(userName, password).equals(user.getPassword())) {
return R.failure(590, "密码错误");
}
return R.success("token", JWTUtils.sign(userName, user.getPassword()));
}
}
定义用户控制器
@RestController
@RequestMapping("user")
public class UserController {
@RequestMapping("save")
@RequiresPermissions("sys:user:save")
public R save() {
return R.success();
}
@RequestMapping("delete")
@RequiresPermissions("sys:user:delete")
public R delete() {
return R.success();
}
}
整个层级效果

登录测试
错误演示  正确演示 
鉴权演示
save接口和delete接口分别需要sys:user:save权限和sys:user:delete权限才能访问。  这里为了方便演示,查询权限的业务类写死了,任意用户都只有save权限  由于查询权限业务写死,任意用户都有save权限,而save接口刚好需要save权限才能访问,所以我们可以正常访问。  但delete接口就需要delete权限,而我们的业务写死了,只有save权限,这个时候访问delete接口就没有权限访问。 
Shiro注解权限控制
RequiresAuthentication:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证(一般指需要登录)。RequiresGuest:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。RequiresPermissions:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。RequiresRoles:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常(下面列出解决办法)。RequiresUser:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
数据表脚本
如果想模拟真实业务通过数据库查询用户信息以及角色和权限,以下为对应实体类的数据表脚本。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` int NOT NULL AUTO_INCREMENT,
`perms` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;
INSERT INTO `menu` VALUES (1, 'sys:user:save');
INSERT INTO `menu` VALUES (2, 'sys:user:delete');
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int NOT NULL,
`role_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色字符串',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;
INSERT INTO `role` VALUES (1, 'root');
INSERT INTO `role` VALUES (2, 'admin');
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`id` int NOT NULL AUTO_INCREMENT,
`rid` bigint NULL DEFAULT NULL COMMENT '角色ID',
`mid` bigint NULL DEFAULT NULL COMMENT '权限ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;
INSERT INTO `role_menu` VALUES (1, 1, 1);
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '盐值',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
INSERT INTO `user` VALUES (1, 'admin', 'dc483e80a7a0bd9ef71d8cf973673924', NULL);
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`uid` bigint NULL DEFAULT NULL COMMENT '用户ID',
`rid` bigint NULL DEFAULT NULL COMMENT '角色ID',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;
INSERT INTO `user_role` VALUES (1, 1, 1);
SET FOREIGN_KEY_CHECKS = 1;
教程到此结束~
本文教程案例下载:https://download.csdn.net/download/qq_31762741/85384639
|