目录
一、为什么要了解权限框架
二、shiro介绍
三、环境准备
3.1 数据库表以及相关数据
3.2、shiro环境准备
四、自定义登录
第一步:需要先完成一些简单的配置:
第二步: 自定义登录方法
第三步:页面按钮隐藏
五、MD5加密算法加密解密过程
过程中的一个报错:
传送门:
权限管理框架属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则用户可以访问而且只能访问自己被授权的资源。
目前常见的权限框架有Shiro和Spring Security,本篇文章记录springboot整合shiro,实现简单的权限控制。
Shiro全称是Apache Shiro,是一款灵活、强大的安全框架。方便简洁的处理身份认证、授权、加密等。
shiro的三个组件:
Subject 【主体】:指代当前用户【人、爬虫等】
SecurityManager【安全管理器】:管理所有的Subject、具体安全操作的真正执行者。
Reamls:本质是一个安全的DTO,用于进行权限、认证信息;
通过Subject来进行认证和授权,而Subject又委托给SecurityManager; 需要给Shrio的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
模拟场景【基于权限】:
1、可以跳转到add页面,说明拥有add权限。
2、可以跳转到update页面,说明拥有update权限。
3、拥有add权限只展示add的链接、拥有update权限只展示update的链接;
模拟场景【基于角色】:
1、拥有admin身份进入add、update,select页面,
2、拥有user身份只可以进入select页面。
实现效果:
环境:
jdk 17
Maven 3.8.6
Mysql 8.x
IDEA2021
springboot 2.7.0
一共五张表:用户表、角色表、权限表、用户角色表、角色权限表。初始化SQL脚本在最后的传送门。
当前数据库数据:【用户->身份->权限】
张三,角色是admin 拥有权限有add
李四,角色是user,拥有权限有update
1、导入必要依赖、导入springboot-shiro的整合相关依赖依赖
org.apache.shiro
shiro-spring-boot-starter
1.10.0
org.apache.shiro
shiro-ehcache
1.7.1
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.4
mysql
mysql-connector-java
cn.hutool
hutool-all
5.7.11
org.springframework.boot
spring-boot-starter-thymeleaf
2.6.4
com.github.theborakompanioni
thymeleaf-extras-shiro
2.0.0
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
2、配置文件的一一些必要参数
server:
port: 8080
spring:
thymeleaf:
mode: HTML
cache: false
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3308/boot_mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&AllowPublicKeyRetrieval=True
username: root
password: root
debug: true
mybatis:
mapperLocations: mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #日志输出
map-underscore-to-camel-case: true #开启驼峰映射
3、 页面资源
准备页面资源:index.html、login.html。启动项目来到首页、不用登录登录情况下可以访问任意页。
项目目录结构:
1、新建UserReam类,继承AuthorizingRealm ,并重写他的认证和授权方法、实现自定义授权认证。
/** * 自定义UserRealm,用户认证授权登录 * @author Alex */ public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token ) throws AuthenticationException { System.err.println("执行了+==========>认证AuthenticationInfo"); // 用户名密码数据库里取 UsernamePasswordToken userToken =(UsernamePasswordToken) token; IUser queryUser = new IUser(); queryUser.setUserName(userToken.getUsername()); List
userList = userService.selectUser(queryUser); if(CollectionUtils.isEmpty(userList)){ return null; }else { IUser user = userList.get(0); System.err.println("user:"+user); // 密码认证 简单的equals比较 return new SimpleAuthenticationInfo(user.getUserName(), user.getPassWord(),""); } } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.err.println("执行了+==========>授权doGetAuthenticationInfo"); String username = (String)principals.getPrimaryPrincipal(); System.err.println("username"+username); IUser queryUser = new IUser(); queryUser.setUserName(username); // 根据用户名获取身份、再由身份获取权限 List roles = userService.selectRolesByUser(queryUser); if(CollectionUtils.isEmpty(roles)){ return null; }else { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); roles.forEach(role -> { simpleAuthorizationInfo.addRole(role.getName()); //权限信息 List perms = userService.selectPermsByRole(role); if (!CollectionUtils.isEmpty(perms)) { perms.forEach(permission -> { simpleAuthorizationInfo.addStringPermission(permission.getPermission()); }); } }); return simpleAuthorizationInfo; } } } 2、新建ShiroConfiguration配置类,
配置类里创建了工厂对象、安全对象、自定Ream等bean对象、 shiro内置了五个过滤器,可对资源、请求接口等进行拦截
/** * shiro配置类 * @author Alex */ @Configuration public class ShiroConfiguration { /** * 工厂对象3 */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //给filter设置安全管理 bean.setSecurityManager(defaultWebSecurityManager); /** * * 基于路径拦截资源 * anon 无需认证 * authc 必须认证 * user 记住我功能 * perms 拥有对某个资源 * roles 用某个角色权限 */ Map
map = new HashMap<>(); map.put("/index","authc"); map.put("/toLogin","anon"); map.put("/","authc"); map.put("/toAdd","perms[add]"); map.put("/toUpdate","perms[update]"); map.put("/toSelect", "roles[admin]"); //更改默认的登录请求路径 bean.setLoginUrl("/toLogin"); //未授权请求路径 bean.setUnauthorizedUrl("/unauthorized"); bean.setFilterChainDefinitionMap(map); return bean; } /** * 安全对象2 */ @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 管理realm securityManager.setRealm(userRealm); return securityManager; } /** * 创建realm对象,先创建,再接管1 */ @Bean(name = "userRealm") public UserRealm userRealm(){ return new UserRealm(); } /** * 页面的Shiro标签生效 * @return */ @Bean public ShiroDialect shiroDialect(){ return new ShiroDialect(); } }
shiro登录过程
传统登录功能:
shiro拿到用户信息后。不是直接调用业务逻辑层的方法。现由SecurityUtils拿到登录对象、由对象取执行login方法,由于UserRealm 继承了AuthorizingRealm、所以登录操作被拦截、完成认证操作。login方法会抛出需要认证失败的异常。根据异常信息可以给前端对应的提示。
@PostMapping("/login") public String login(String username, String password, Model model){ //获取用户主体对象 Subject subject = SecurityUtils.getSubject(); // 将输入的用户名+密码封装成一个token UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); model.addAttribute("userName",username); IUser user = new IUser(); user.setUserName(username); List
roles = userService.selectRolesByUser(user); List perms = userService.selectPermsByRole(roles.get(0)); model.addAttribute("role",roles.get(0).getName()); model.addAttribute("perms",perms); return "index"; }catch (UnknownAccountException e){ e.printStackTrace(); model.addAttribute("msg","用户名错误"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); model.addAttribute("msg","密码错误"); } return "login"; }
/** * 自定义UserRealm,用户认证授权登录 * @author Alex */ public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token ) throws AuthenticationException { System.err.println("执行了+==========>认证AuthenticationInfo"); SimpleAuthenticationInfo info =null; String username = token.getPrincipal().toString(); IUser queryUser = new IUser(); queryUser.setUserName(username); List
dbUserList = userService.selectUser(queryUser); if(CollectionUtils.isNotEmpty(dbUserList)){ IUser dbUser = dbUserList.get(0); // 将注册时保存的随机盐构造ByteSource对象 info = new SimpleAuthenticationInfo(dbUser.getUserName(),dbUser.getPassWord(),this.getName()); // info = new SimpleAuthenticationInfo(dbUser.getUserName(),dbUser.getPassWord(),new SimpleByteSource(dbUser.getSalt()),this.getName()); } return info; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.err.println("执行了+==========>授权doGetAuthenticationInfo"); String username = (String)principals.getPrimaryPrincipal(); System.err.println("username"+username); IUser queryUser = new IUser(); queryUser.setUserName(username); // 根据用户名获取身份、再由身份获取权限 List roles = userService.selectRolesByUser(queryUser); if(CollectionUtils.isEmpty(roles)){ return null; }else { SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); roles.forEach(role -> { simpleAuthorizationInfo.addRole(role.getName()); //权限信息 List perms = userService.selectPermsByRole(role); if (!CollectionUtils.isEmpty(perms)) { perms.forEach(permission -> { simpleAuthorizationInfo.addStringPermission(permission.getPermission()); }); } }); return simpleAuthorizationInfo; } } }
完成了简单资源授权,为了前端展示,把对应的数据存到model
IUser user = new IUser(); user.setUserName(username); List
roles = userService.selectRolesByUser(user); List perms = userService.selectPermsByRole(roles.get(0)); model.addAttribute("role",roles.get(0).getName()); model.addAttribute("perms",perms);
效果如下:
用户访问首页会被拦截、并跳转至登录页。
以李四的账户登录情况也类似;
使用注解检查用户是否有可访问此接口
@RequiresRoles
@RequiresRoles
注解用于配置接口要求用户拥有某(些)角色才可访问,它拥有两个参数
参数 类型 描述 value String[] 角色列表 logical Logical 角色之间的判断关系,默认为Logical.AND
@RequiresPermissions
@RequiresPermissions
注解用于配置接口要求用户拥有某(些)权限才可访问,它拥有两个参数
参数 类型 描述 value String[] 权限列表 logical Logical 权限之间的判断关系,默认为Logical.AND
可单独使用、也可组合使用
比如:只有拥有add权限、拥有admin身份的主体才可以访问toAdd方法;
@GetMapping("/toAdd")
@RequiresPermissions("add")
@RequiresRoles("admin")
public String toAdd(){
return "user/add";
}
shiroConfig中创建bean对象
/**
* 页面的Shiro标签生效
* @return
*/
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
页面引入头文件
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
使用
shiro:hasRoleshiro:hasPermission
首页
hello,shiro,我是
权限有:
身份是:
基于角色展示
基于权限展示:
登录
退出
效果;
以上本篇主体内容了
补充:为了用户信息安全、一般情况下都不会将 密码明文存入数据库,二是通过加密算法将明文加密后的字符串存入数据库、常见的加密算法有:
单向散列加密算法:MD5;
对称加密:
非对称加密:
下面是对MD5+随机盐加密及解密过程的叙述。
MD5加密算法是hash算法的一种,原理是通过字符串通过MD5加密后得到一个消息摘要。
通过MD5算法加密后的字符串本身是不可逆的,但是对同一个字符串加密后得到的密文是一致的、所以许多简单的密码可以用枚举实现解密。故加密时将明文+随机盐达到新的字符串尽量唯一从而增加安全性;
这里使用了一了开源hutools工具包生成随机数。hutools可以生成二维码、验证码等等,当然也可以自己实现。
@Test
public void testSlat(){
//生成6位随机盐
String salt = RandomUtil.randomString(6);
System.err.println("salt:"+salt);
//加密密码:原始密码+盐+Hash散列
String MdsPwd = new Md5Hash("123", salt, 1024).toHex();
//设置加密后的密码
System.err.println(MdsPwd);
}
将注册时保存的随机盐一并带上
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token ) throws AuthenticationException { System.err.println("执行了+==========>认证AuthenticationInfo"); SimpleAuthenticationInfo info =null; String username = token.getPrincipal().toString(); IUser queryUser = new IUser(); queryUser.setUserName(username); List
dbUserList = userService.selectUser(queryUser); if(CollectionUtils.isNotEmpty(dbUserList)){ IUser dbUser = dbUserList.get(0); // 将注册时保存的随机盐构造ByteSource对象 info = new SimpleAuthenticationInfo(dbUser.getUserName(),dbUser.getPassWord(),new SimpleByteSource(dbUser.getSalt()),this.getName()); } return info; }
Submitted credentials for token [org.apache.shiro.authc.UsernamePasswordToken - 张三, rememberMe=false] did not match the expected credentials. at org.apache.shiro.realm.AuthenticatingRealm.assertCredentialsMatch(AuthenticatingRealm.java:603)
整体大意就是密码错误。
原因:是因为修改了shiro的认证策略后、获取UserRealm对象的方式还是以前的
new UserRealm(),这种方式比较密码是将输入的密码和数据库中密码进行简单的equals比较。而不是盐加+哈希散列的认证方式。所以只需要在获取userRealm对象的时候添加一下加密设置就好了。
1、配置类中注入CredentialsMatcher 对象。
/** * 进行加密的设置 */ @Bean public CredentialsMatcher credentialsMatcher(){ HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); // 指定加密算法 matcher.setHashAlgorithmName("md5"); // 迭代次数 matcher.setHashIterations(1024); return matcher; }
2、
/** * 创建realm对象,先创建,再接管1 */ @Bean(name = "userRealm") public UserRealm userRealm(){ UserRealm userRealm = new UserRealm(); // 加密设置 userRealm.setCredentialsMatcher(credentialsMatcher());; return userRealm; }
3、测试方法:
@Autowired ShiroConfiguration configuration; @Test public void test(){ // 创建安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(configuration.userRealm()); // 注入安全工具类 SecurityUtils.setSecurityManager(defaultSecurityManager); // 拿到登录主体 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("张三", "123"); try{ subject.login(token); System.err.println("登录成功"); } catch (Exception e){ System.err.println("登录失败"); e.printStackTrace(); } }
初始化SQL脚本
使用IDEA新建一个springboot项目
springboot整合thymeleaf
springboot整合mybatis
springboot整合SpringSecurity并实现简单权限控制