序言
本文讲解如何使用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
|