Springboot整合shiro
1.概述
Apache Shiro 是一款 Java 安全框架,不依赖任何容器,可以运行在 Java SE 和 Java EE 项目中,它的主要作用是用来做身份认证、授权、会话管理、缓存和加密等操作。
和SpringSecurity的作用大概是一致的,但SpringSecurity由于其功能丰富而复杂则多用在大型项目中,而Shiro则适用于中小型项目中,上手快。
2.shiro基本原理与演示
2.1核心组件
1)Subject(用户):当前的操作用户,通过Subject currentUser = SecurityUtils.getSubject()获取。
2)SecurityManager(安全管理器):Shiro 的核心部分,负责安全认证与授权。
3)Realms(数据源):充当与安全管理间的桥梁,查找数据源进行验证和授权操作。
4)Authenticator(认证器):用于认证,从 Realm 数据源取得数据之后进行执行认证流程处理。AuthenticationInfo存储用户的角色信息集合,核心方法是doGetAuthenticationInfo
。
5)Authorizer(授权器):用户访问控制授权,决定用户是否拥有执行指定操作的权限。AuthorizationInfo存储角色的权限信息集合,核心方法是doGetAuthorizationInfo
。
6)SessionManager(会话管理器):支持会话管理。
7)CacheManager(缓存管理器):用于缓存认证授权信息。
8)Cryptography(加密组件):提供了加密解密的工具包,用于密码的加密。
2.2Shiro认证 - 基于ini认证
这里先创建SpringBoot项目,也可以直接使用普通的maven项目,由于后续需要进行整合SpringBoot,故这里直接以SpringBoot为基础。[版本说明:SpringBoot 2.7.2,shiro 1.5.2]
1)先导入shiro的核心依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.2</version> </dependency>
2)在资源目录下创建配置文件shiro-au.ini
#用户的身份、凭据 [users] zhangsan=555 xiaoluo=666
其中#用来注释,而中括号用来标记类型,比如这里是标记用户信息,用户名和密码使用等号的方式连接。
3)新建测试方法, 使用用户名和密码验证登录
public void testLogin() { //创建Shiro的安全管理器,是shiro的核心 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //加载shiro.ini配置,得到配置中的用户信息(账号+密码) IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini"); securityManager.setRealm(iniRealm); //把安全管理器注入到当前的环境中 SecurityUtils.setSecurityManager(securityManager); //获取subject主体对象,无论有无登录都可以获取到,但是需要属性来判断登录状态 Subject subject = SecurityUtils.getSubject(); System.out.println("未登录时认证状态:" + subject.isAuthenticated()); //创建令牌(携带登录用户的账号和密码) UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666"); //执行登录操作(将用户的和 ini 配置中的账号密码做匹配) subject.login(token); System.out.println("登录成功认证状态:" + subject.isAuthenticated()); //登出 subject.logout(); System.out.println("退出后认证状态:"+subject.isAuthenticated()); }
打印结果如下
上述用户名和密码都是正确的,实际上当用户名或密码有一个不正确时就不会打印所有的信息,而是会在执行登录操作时抛出异常
- 当用户名错误,抛异常UnknownAccountException
- 当密码错误,抛异常IncorrectCredentialsException
下面看源码,先进入login的方法:
这里调用了安全管理器的login方法,进入
这里调用了认证的方法,进入
继续深入
这里就选择抽象的类进入
进入后,发现其出现了Realm,开始进行数据源的验证
最终找到了其调用的认证的方法
2.3Shiro认证 - 基于Realm认证
在实际的应用场景中,当然不会把用户名和密码放在配置文件中,而是需要结合数据库进行验证。上述只是说明验证登录的流程,需要自定义数据源进行用户名和密码的验证
1)创建用户对象
package com.zxh.test.entity; import lombok.Data; @Data public class User { private String username; private String password; private String addr; }
2)自定义Realm
package com.zxh.test.controller; import com.zxh.test.entity.User; 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.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; public class MyRealm extends AuthorizingRealm { //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.获取登录用户名 String username = (String) token.getPrincipal(); //2.以用户名为条件查询数据库,得到用户信息 //这里模拟数据 User user = new User(); user.setUsername("xiaoluo"); user.setPassword("123"); //封装成一个认证info对象 //判断用户是否为空 if (user != null) { return new SimpleAuthenticationInfo( user, user.getPassword(), super.getName() ); } return null; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } }
这里为了演示的简单及说明原理,并没有真正的去连接数据库进行数据的验证
3)验证登录
@Test public void testLogin2() { //创建Shiro的安全管理器 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //使用自定义的数据源 MyRealm myRealm = new MyRealm(); securityManager.setRealm(myRealm); //把安全管理器注入到当前的环境中 SecurityUtils.setSecurityManager(securityManager); //获取到subject主体对象 Subject subject = SecurityUtils.getSubject(); System.out.println("认证状态:" + subject.isAuthenticated()); //创建令牌 UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666"); //执行登录操作 subject.login(token); System.out.println("认证状态:" + subject.isAuthenticated()); }
打印的结果是上一小节是一样的。
2.4Shiro授权 - 基于ini授权
在shiro-au.ini文件中密码后面添加角色即可,以逗号分隔
[users] zhangsan=555,role1,role2,role3 xiaoluo=666,role2
测试方法
@Test public void testRole() { DefaultSecurityManager securityManager = new DefaultSecurityManager(); IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini"); securityManager.setRealm(iniRealm); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666"); subject.login(token); System.out.println(subject.hasRole("role1")); System.out.println(subject.hasRole("role2")); }
用户xiaoluo只有role2角色,故打印结果是false,true 。
除了判断角色外,还可以判断权限,在ini文件添加角色的权限表达式
[roles] # 权限表达式 资源:操作 role1=*:* role2=user:add,user:select
分别用两个账号进行测试
@Test public void testPermitted() { DefaultSecurityManager securityManager = new DefaultSecurityManager(); IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini"); securityManager.setRealm(iniRealm); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("xiaoluo", "666"); subject.login(token); System.out.println(subject.isPermitted("user:edit"));//false System.out.println(subject.isPermitted("user:add"));//true System.out.println(subject.isPermitted("user:select"));//true } @Test public void testPermitted2() { DefaultSecurityManager securityManager = new DefaultSecurityManager(); IniRealm iniRealm = new IniRealm("classpath:shiro-au.ini"); securityManager.setRealm(iniRealm); SecurityUtils.setSecurityManager(securityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "555"); subject.login(token); System.out.println(subject.isPermitted("user:edit"));//true System.out.println(subject.isPermitted("user:add"));//true System.out.println(subject.isPermitted("user:select"));//true }
可以看出,xiaoluo只有部分权限,而zhangsan的权限是*,也就意味着拥有所有的权限。
2.5 Shiro加密
上面的例子中密码都是明文的,而shiro也提供了加密的方法,这里以MD5加密为例。
@Test public void MD5Test() { String password = "123"; //使用其封装好的加密类 Md5Hash md5Hash = new Md5Hash(password); System.out.println(md5Hash); //加盐加密 Md5Hash md5Hash2 = new Md5Hash(password, "000000"); System.out.println(md5Hash2); //加盐加密并多次加密 Md5Hash md5Hash3 = new Md5Hash(password, "000000", 6); System.out.println(md5Hash3); //Md5Hash的父类是SimpleHash,也可以使用父类加密 其加密的密文和上面是一样的 SimpleHash simpleHash = new SimpleHash("MD5", password); SimpleHash simpleHash2 = new SimpleHash("MD5", password, "000000"); SimpleHash simpleHash3 = new SimpleHash("MD5", password, "000000", 6); }
shiro默认的登录认证是不加密的,若需要进行加密验证,则需要自定义认证。
3.整合Spingboot-前后端一体
结合thymeleaf进行说明。
3.1准备工作
1)创建SpringBoot项目,引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
2)创建实体类User
package com.zxh.test.entity; import lombok.Data; @Data public class User { private String username; private String password; private String salt; }
3)配置数据源等信息
spring.datasource..url=jdbc:mysql://localhost:3306/db2023_test?useUnicode=true&characterEncoding=UTF-8 spring.datasource..username=root spring.datasource..password=123456 mybatis.mapper-locations=classpath:mapper/*Mapper.xml
4)创建数据库和用户表
CREATE TABLE `t_user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `password` varchar(200) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, `salt` varchar(100) CHARACTER SET utf8 COLLATE utf8_bin DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;
INSERT INTO `db2023_test`.`t_user`(`id`, `username`, `password`, `salt`) VALUES (2, 'zs', '6cf1387af615e766d100860f3546813f', '000000');
这里的密码已通过上述的shiro的6次迭代加盐加密。
3.2引入shiro
1)导入依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.5.2</version> </dependency>
2)创建UserService服务
package com.zxh.test.service; import com.zxh.test.dao.UserDao; import com.zxh.test.entity.User; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service @Slf4j public class UserService { @Autowired private UserDao userDao; public User selectByName(String name){ return userDao.selectByName(name); } }
其调用dao的代码在此省略,只展示xml
<select id="selectByName" resultType="com.zxh.test.entity.User"> select * from t_user where username = #{name} </select>
3)自定义realm
package com.zxh.test.config; import com.zxh.test.entity.User; import com.zxh.test.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.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * shiro自定义配置realm */ @Component public class MyRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String username = (String) authenticationToken.getPrincipal(); User user = userService.selectByName(username); if (user != null) { ByteSource salt = ByteSource.Util.bytes(user.getSalt()); return new SimpleAuthenticationInfo(username, user.getPassword(), salt, super.getName()); } return null; } }
这里的密码通过shiro的md5加盐加密
4)配置shiro
package com.zxh.test.config; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.HashMap; import java.util.Map; /** * shiro类 */ @Configuration public class ShiroConfig { @Autowired private MyRealm myRealm; //创建安全管理器 @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager() { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //创建加密对象,指定加密策略 HashedCredentialsMatcher matcher=new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("md5"); matcher.setHashIterations(6); myRealm.setCredentialsMatcher(matcher); securityManager.setRealm(myRealm); return securityManager; } //配置Shiro过滤器 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //给ShiroFilter配置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> map = new HashMap<>(); //配置系统公共资源 map.put("/userLogin", "anon");//anon表示这个资源无需认证//配置系统受限资源 map.put("/**", "authc");//authc表示这个资源需要认证和授权 shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } }
6)创建controller
@Controller public class UserController { @PostMapping("/userLogin") @ResponseBody public String userLogin(String username, String password) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); return "登录成功"; } catch (Exception e) { if (e instanceof UnknownAccountException || e instanceof IncorrectCredentialsException) { return "用户名或密码错误"; } return "登录失败"; } } }
7)启动项目后,使用postman以表单方式提交
当用户名和密码都正确时,会显示登录成功,反正则显示用户名或密码错误。
3.3引入前端页面
1)在templates目录下新建两个页面
涉及到权限,必须有页面进行支撑。
login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <form action="/userLogin" method="post"> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="password" name="password"></p> <p><button type="submit" >登录</button></p> </form> </body> </html>
main.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>主页面</title> </head> <body> <p>登录成功了,我 是主页,登录用户:<span th:text="${session.user}"></span></p> </body> </html>
2)在controller对登录的接口进行修改,并添加两个视图解析
@PostMapping("/userLogin") //@ResponseBody 需要去掉此注解 public String userLogin(String username, String password, HttpSession session) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); //设置session信息,供前端使用 session.setAttribute("user", token.getPrincipal()); // return "登录成功"; return "main"; } catch (Exception e) { if (e instanceof UnknownAccountException || e instanceof IncorrectCredentialsException) { return "用户名或密码错误"; } return "登录失败"; } } @GetMapping("/login") public String login() { return "login"; } @GetMapping("/main") public String main() { return "main"; }
此类并没有使用@RestController注解,原因就是这里包含了视图解析,不能使用此注解。
3)修改shiro的配置类的过滤器
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //给ShiroFilter配置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, String> map = new HashMap<>(); //配置系统公共资源 map.put("/userLogin", "anon");//anon表示这个资源无需认证 map.put("/login", "anon"); //配置系统受限资源 map.put("/**", "authc");//authc表示这个资源需要认证和授权 shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; }
启动后直接访问/main会直接跳转到登录页面
输入正确的用户名和密码即可进入到主页
3.4 多realm认证
上述只是根据用户名和密码校验登录信息,而在实际的场景中,可能会存在手机号等其他多种方式的登录,故可配置多种数据源方式的登录。
shiro会调用内部组件AuthenticationStrategy进行判断,其在身份认证尝试中被调用4次,同时也聚合了所有的Realm的结果信息封装到AuthenticationInfo并返回,依次作为Subject的身份信息。
shiro的3中认证策略如下表:
策略 | 说明 |
AtlLeastOneSuccessfulStrateay | 默认的策略。只要有一个(或多个)验证成功,则认证视为成功 |
FirstSuccessfulStrategy | 第一个Realm 验证成功,整体认证将视为成功,且后续 Realm 将被忽略 |
AllSuccessfulStrategy | 所有 Realm 成功,认证才视为成功 |
那么如何更改默认的认证策略呢?只需在安全管理器中设置即可。由于其默认的策略已符合大部分需求,故在此简单说明配置,不具体展开:
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //创建认证对象 并设置认证策略 ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator(); modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); securityManager.setAuthenticator(modularRealmAuthenticator);
与此同时,设置realm的方法要由单个变成多个,
securityManager.setRealms(Arrays.asList(myRealm, myRealm2));
UnauthorizedException:没有权限时的异常