
本文讲解如何使用SpringBoot整合Shiro框架来实现认证及权限校验,但如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot整合Shiro框架的时候会联合JWT一起搭配使用。
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份
认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
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(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被篡改、确保安全性 例如 计算出来的签名,一个字符串
项目环境
pom依赖
JWTUtilorg.projectlombok lombok org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.apache.shiro shiro-spring-boot-web-starter 1.4.1 org.apache.shiro shiro-ehcache 1.4.1 com.auth0 java-jwt 3.2.0 com.alibaba fastjson 1.2.15
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);
//设置签名的加密算法:HMAC256
Algorithm algorithm = Algorithm.HMAC256(password);
// 附带username信息
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 {
//token 异常 认证失败
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);
//提交给realm进⾏登⼊,如果错误他会抛出异常并被捕获
getSubject(request, response).login(token);
return true;
}
}
自定义ShiroRealm
自定义的Realm对象,该对象继承于AuthorizingRealm,实现了Shiro具体认证和授权的方法。
另外需要注意:
必须要重写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();
//从token中获取用户名
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 roles = roleService.selectRoleByUserId(user.getId());
for (Role role : roles) {
//将角色添加到授权信息中
info.addRole(role.getRoleKey());
}
//赋予资源
List
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();
// 设置哈希算法名称,这里使用MD5算法
credentialsMatcher.setHashAlgorithmName("MD5");
// 设置哈希迭代,这里迭代2次,相当于 md5(md5(""))
credentialsMatcher.setHashIterations(2);
// 设置存储的凭据16进制编码,需要和生成密码时的一样,默认是 Base64
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);
// 设置自定义Realm
securityManager.setRealm(shiroRealm);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 添加自己的过滤器并且取名为jwt
Map filters = new LinkedHashMap<>();
filters.put("jwt", new JWTFilter());
factoryBean.setFilters(filters);
Map filterChainDefinitionManager = new LinkedHashMap<>();
// 所有请求通过我们自己的JWT Filter
filterChainDefinitionManager.put("
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
// 强制使用cglib,防止重复代理和可能引起代理出错的问题
// https://zhuanlan.zhihu.com/p/29161098
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
aASA.setSecurityManager(securityManager);
return aASA;
}
}
这里开启注解支持需要添加DefaultAdvisorAutoProxyCreator(可选) 和AuthorizationAttributeSourceAdvisor ,DefaultAdvisorAutoProxyCreator也可以选择不加,这里加是因为防止重复代理和可能引起代理出错的问题
认证和授权规则认证过滤器
使用@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 {
// ID
private Integer id;
// 用户名
private String userName;
// 密码
private String password;
// 盐值
private String salt;
}
定义Role
角色实体类
@Data
public class Role {
// ID
private Integer id;
// 角色字符串
private String roleKey;
}
Menu
菜单实体类
@Data
public class Menu {
// ID
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 selectRoleByUserId(Integer id);
}
实现类
@Service
public class RoleServiceImpl implements RoleService {
@Override
public List selectRoleByUserId(Integer id) {
List roles = new ArrayList<>();
Role admin = new Role();
admin.setRoleKey("admin");
roles.add(admin);
return roles;
}
}
这里为了方便演示把角色写死了
MenuService接口
public interface MenuService {
List
实现类
@Service
public class MenuServiceImpl implements MenuService {
@Override
public List
这里为了方便演示把权限写死了
统一结果集工具public class R extends HashMapehcache.xml缓存配置{ 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); } }
配置详解
定义登录接口
@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, "密码错误");
}
//token生成采用加密后的密码,这个和realm中的校验必须一致
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接口就没有权限访问。
如果想模拟真实业务通过数据库查询用户信息以及角色和权限,以下为对应实体类的数据表脚本。
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for menu -- ---------------------------- 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; -- ---------------------------- -- Records of menu -- ---------------------------- INSERT INTO `menu` VALUES (1, 'sys:user:save'); INSERT INTO `menu` VALUES (2, 'sys:user:delete'); -- ---------------------------- -- Table structure for role -- ---------------------------- 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; -- ---------------------------- -- Records of role -- ---------------------------- INSERT INTO `role` VALUES (1, 'root'); INSERT INTO `role` VALUES (2, 'admin'); -- ---------------------------- -- Table structure for role_menu -- ---------------------------- 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; -- ---------------------------- -- Records of role_menu -- ---------------------------- INSERT INTO `role_menu` VALUES (1, 1, 1); -- ---------------------------- -- Table structure for user -- ---------------------------- 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; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'admin', 'dc483e80a7a0bd9ef71d8cf973673924', NULL); -- ---------------------------- -- Table structure for user_role -- ---------------------------- 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; -- ---------------------------- -- Records of user_role -- ---------------------------- INSERT INTO `user_role` VALUES (1, 1, 1); SET FOREIGN_KEY_CHECKS = 1;教程到此结束~
本文教程案例下载:https://download.csdn.net/download/qq_31762741/85384639