Java开发之shiro学习
写在前面,若后续学习的时候要先注意包的名字,我这里的包名为 com.example
,实际你自己的情况可能并不是,要注意修改。
Java 开发之 shiro 学习
0x01 前言
什么是 Shiro?
- Apache Shiro 是一个功能强大、灵活的,开源的安全框架。
- Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 JavaEE 环境。
- Shiro 可以完成,认证,授权,加密,会话管理,Web 集成,缓存等。
- 下载地址:https://shiro.apache.org/
有哪些功能
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
- Session Management(会话管理):特定于用户的会话管理,甚至在非 Web 或 EJB 应用程序。
- Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。
shiro 内部
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager 和 Realm。
- Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。一般 Subject 这里是连前端数据的。
- SecurityManager:管理所有 Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
- Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到 Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个 Realm 来实现认证(authentication)和/或授权(authorization)。
0x02 环境
- maven 3.6.3
- SpringBoot 2.7.1
- jdk8u_312
其余配置如下 pom.xml 所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.1</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
|
0x03 实现思路
- 这一段写的对我自己比较重要吧,因为能让自己熟悉一整个开发过程。
shiro 的实现简单来说分为两个东西,一个是 ShiroConfig
,另一个是 DAORealm
。这个 DAORealm
中的 DAO 也就是我们的实体类。
作为一个身份认证,权限管理的组件,很明显,shiro 这里需要我们去实现的,也就是 DIY 的东西一定是这么几个:
- 身份如何认证 ———— 应当与数据库结合。
- 权限管理 ———— 不同用户具有不同的权限。
- 认证错误 ———— Controller 层接口跳转
所以此处,我们要先实现 DAORealm
这个东西。
DAORealm 的实现
DAORealm 最开始我们可以只是继承 AuthorizingRealm
,并象征性的重写抽象方法。
创建出的 DAORealm 是要拿到 ShiroConfig 里面用的,
- 从封装的角度来说,这里是提供用户名与密码的,调用 SQL 语句进行 CRUD。
ShiroConfig 的实现
重点其实是在这一步的。
ShiroConfig 的实现需要我们对三层进行实现。
这里我们要去实现前面讲的三个模块。
- 创建 realm 对象,需要自定义类
- DefaultWebSecurityManager
- ShiroFilterFactoryBean
我们已经有了 Realm 对象,下面是 DefaultWebSecurityManager,要让 DefaultWebSecurityManager 关联 Realm。
有了 DefaultWebSecurityManager 之后是 ShiroFilterFactoryBean,ShiroFilterFactoryBean 调用 DefaultWebSecurityManager,将其作为安全管理器。
我们的过滤器也是在 ShiroFilterFactoryBean 里面配的,三者都需要加上 @Bean
注解。
基础的就这些了,下面我们看实战。
0x04 Shiro 整合
1. 测试环境
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| package shiro.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping({"/","/index"}) public String toIndex(Model model) { model.addAttribute("msg","hello,shiro"); return "index"; } @RequestMapping("/user/add") public String add() { return "user/add"; } @RequestMapping("/user/update") public String update() { return "user/update"; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <div> <h1>首页</h1> <p th:text="${msg}"></p> <hr> <a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a> </div> </body> </html>
|
1 2 3 4 5 6 7 8 9 10
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>add</h1> </body> </html>
|
1 2 3 4 5 6 7 8 9 10
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>update</h1> </body> </html>
|
2. 导入 Shiro 包并整合
1 2 3 4 5
| <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.5.3</version> </dependency>
|
要上到下实现,并建立连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); bean.setSecurityManager(defaultWebSecurityManager); return bean; } @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(userRealm); return securityManager; } @Bean public UserRealm userRealm() { return new UserRealm(); } }
|
3. 实现登录拦截
前面在实现思路里我说了,是到 ShiroFilterFactoryBean 这个方法里面去定义过滤器的一堆东西,增加配置如下。
1 2 3 4
| Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/user/add","authc"); filterMap.put("/user/update","authc"); bean.setFilterChainDefinitionMap(filterMap);
|
authc 是一种认证方式,还有一些其他的认证方式如下
1 2 3 4 5 6 7 8
| // 添加 shiro 的内置过滤器 /* anon:无需认证即可访问 authc:必须认证了才能访问 user:必须拥有 记住我功能才能使用 perms:拥有对某个资源的权限才能访问 role:拥有某个角色权限 */
|
此时,我们的拦截配置已经生效了,这时候跑程序的话,点击 add 或者 update 的 href 是有 404 的拦截的。
如果拦截成功了,从交互角度来说,是要让其进行登录的,所以我们先写一个 login.html 的界面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录页面</title> </head> <body> <h1>登录</h1> <hr> <form action=""> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p> <p>密码:<input type="submit"></p> </form> </body> </html>
|
- 在MyController中添加去往
login.html
的接口
1 2 3 4
| @RequestMapping("/toLogin") public String toLogin() { return "login"; }
|
- 在
ShiroConfig
中的 getShiroFilterFactoryBean
方法中添加如下配置
1 2
| bean.setLoginUrl("/toLogin");
|
如此一来,我们在点击 add 与 update 的时候就会去到 login.html 了。
4. 实现用户认证
在 MyController
中编写用户提交表单之后处理,也就是对于 /login
接口的身份认证。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @RequestMapping("/login") public String login(String username, String password, Model model) { Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { subject.login(token); return "index"; } catch (UnknownAccountException e) { model.addAttribute("msg","用户名错误"); return "login"; } catch (IncorrectCredentialsException e) { model.addAttribute("msg","密码错误"); return "login"; } }
|
修改 login.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>登录页面</title> </head> <body> <h1>登录</h1> <hr> <p th:text="${msg}" style="color: red;"></p> <form th:action="@{/login}"> <p>用户名:<input type="text" name="username"></p> <p>密码:<input type="text" name="password"></p> <p>密码:<input type="submit"></p> </form> </body> </html>
|
用户认证编写 UserRealm
中的认证(doGetAuthenticationInfo),这里我们先使用固定的 username 与 password,后续再与数据库整合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthorizationInfo"); String name = "root"; String password = "123456"; UsernamePasswordToken userToken = (UsernamePasswordToken) token; if (!userToken.getUsername().equals(name)) { return null; } return new SimpleAuthenticationInfo("",password,""); }
|
5. Shiro 整合 Mybatis
导入依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.23</version> </dependency>
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency>
|
配置文件 application.yml 的编写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| spring: datasource: username: root password: url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 mybatis: type-aliases-package: com.example.pojo mapper-locations: classpath:mapper/*.xml
|
User 实体类的编写
1 2 3 4 5 6 7 8
| @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; }
|
UserMapper 接口编写
1 2 3 4 5
| @Repository @Mapper public interface UserMapper { public User queryUserByName(String name); }
|
UserMapper.xml 配置映射
1 2 3 4 5 6 7 8 9 10 11 12
| <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper"> <select id="queryUserByName" parameterType="String" resultType="User"> select * from mybatis.user where name=#{name}; </select> </mapper>
|
UserService 代理类接口
1 2 3 4
| public interface UserService { public User queryUserByName(String name); }
|
UserServiceImpl 业务逻辑
1 2 3 4 5 6 7 8 9 10
| @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
|
测试环境
1 2 3 4 5 6 7 8 9 10 11
| @SpringBootTest class ShiroSpringbootApplicationTests { @Autowired UserService userService; @Test void contextLoads() { System.out.println(userService.queryUserByName("drunkbaby")); } }
|
这时候运行的话,是有对应自己的数据库出来的,如果报错了的话,看一看配置文件有没有写对,路径有没有写对。
UserRealm
连接真实数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Autowired UserService userService;
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证doGetAuthorizationInfo"); UsernamePasswordToken userToken = (UsernamePasswordToken) token; User user = userService.queryUserByName(userToken.getUsername()); if (user == null) { return null; } return new SimpleAuthenticationInfo("",user.getPwd(),""); }
|
6. Shiro 实现用户授权
ShiroConfig
中的 getShiroFilterFactoryBean
方法添加认证代码
1 2 3
| filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]");
|
这样是为了设置 401 的 Unauthorized 拦截
添加为授权页面
1 2 3 4 5
| @RequestMapping("/noauth") @ResponseBody public String unauthorized() { return "未经授权,无法访问此页面"; }
|
ShiroConfig
中的 getShiroFilterFactoryBean
方法中添加
1 2
| bean.setUnauthorizedUrl("/noauth");
|
跑一下启动器
所以需要在 UserRealm 中为用户进行真正授权
往数据库中的 user 表添加上 perms 字段,用于授权;然后修改对应的 pojo 中的 User 类增加对应属性。
UserRealm 类的修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行了=>授权doGetAuthorizationInfo"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Subject subject = SecurityUtils.getSubject(); User currentUser = (User)subject.getPrincipal(); info.addStringPermission(currentUser.getPerms()); return info; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { ...... return new SimpleAuthenticationInfo(user,user.getPwd(),""); } }
|
数据库的 perms 要先写好权限的,比如 user:add
,user:update
,不然会 500 报错。
0x05 参考资料
SpringBoot整合框架 – JohnFrod’s Blog
【狂神说Java】SpringBoot最新教程IDEA版通俗易懂