一、环境配置
- 一般springboot环境配置文章指路:【Spring Boot】快速上手SpringBoot
- 导入shiro整合spring的依赖:
<!--shiro整合spring-->
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.6.0</version>
</dependency>
- 导入Thymeleaf引擎:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 项目结构:
二、Shiro使用
- Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能,对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。与【Spring Boot】Spring Security功能类似,可以自己选择一个使用。
- 基本功能:
功能 | 介绍 |
---|---|
Authentication | 身份认证 / 登录,验证用户是不是拥有相应的身份; |
Authorization | 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限; |
Session Management | 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的; |
Cryptography | 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储; |
Web Support | Web 支持,可以非常容易的集成到 Web 环境; |
Caching | 缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率; |
Concurrency | shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; |
Testing | 提供测试支持; |
Run As | 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; |
Remember Me | 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。 |
- 如何使用Shiro?
-
Subject:
主体,代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者; -
SecurityManager:
安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器; -
Realm:
域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色 / 权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。
最简单的一个 Shiro 应用:
应用代码通过 Subject 来进行认证和授权,而 Subject 又委托给 SecurityManager;我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能得到合法的用户及其权限进行判断。
步骤:
- 自定义一个Realm类:UserRealm.java
需要继承AuthorizingRealm类,重写两个方法,分别对应授权和认证功能。
//自定义的Realm类:继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证");
String name = "root";
String password = "123456";
//userToken来自前端传递来的数据
UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
if (!userToken.getUsername().equals(name)){
return null;
}
return new SimpleAuthenticationInfo("",password,"");
}
}
- 配置ShiroConfig类:ShiroConfig.java
我们先是写Realm===》Realm交给SecurityManager=》SecurityManager交给Subject=》完成
拦截了/user/*下的请求,自由认证了,登录了才可以访问,不然直接访问会跳到登录页面
@Configuration
public class ShiroConfig {
// Subject用户
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*anon:无需认证就可以访问
authc:必须认证了才能让问
user: 必须拥行记住我功能才能用
perms:拥有对某个资源的权限才能访问;
role:拥行某个角色权限才能访问
*/
//拦截
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
//设置登录的请求
bean.setLoginUrl("/to_login");
return bean;
}
// SecurityManager管理所有用户2
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
// Realm连接数据 需要自定义类1
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
- controller类:UserController.java
注意最后一个就是将我们前端的信息传递给Reaml类中认证。
@Controller
public class UserController {
@RequestMapping({"/","/index"})
public String index(){
return "index";
}
@RequestMapping("/to_login")
public String toLogin(){
return "login";
}
@RequestMapping("/user/add")
public String userAdd(){
return "user/add";
}
@RequestMapping("/user/update")
public String userUpdate(){
return "user/update";
}
@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);//执行登录方法,如果没有异常就说明OK
return "index";
}catch (UnknownAccountException e){
model.addAttribute("msg","用户名错误");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "login";
}
}
}
- 登录页面:login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div align="center">
<h1>登录</h1>
<span th:text="${msg}"></span>
<form th:action="@{/login}" method="post">
<p>用户名:<input type="text" name="username" ></p>
<p>密码:<input type="password" name="password" ></p>
<input type="submit" value="登录">
</form>
</div>
</body>
</html>
三、配合数据库使用Shiro
前面自定义Realm类中认证自定义了一个root用户,我们现在需要这些用户来自数据库。
- 导入数据库相关的依赖和myabtis-springboot的依赖
<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.2.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 连接好数据库,用mybatis实现根据name查询用户的方法
application.yaml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/hz?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
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:监控统计、log4j:日志记录、wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 整合Mybatis
mybatis:
type-aliases-package: com.gaolang.springbootshiro.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
UserMapper.java
@Controller
@Mapper
public interface UserMapper {
User getUserByName(String name);
}
UserMapper.xml
<?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">
<!--namespace=绑定一个对应的dao接口/mapper接口-->
<mapper namespace="com.gaolang.springbootshiro.mapper.UserMapper">
<!--select查询-->
<select id="getUserByName" resultType="User">
select * from user_login where name=#{name}
</select>
</mapper>
UserService.java
public interface UserService {
User getUserByName(String name);
}
UserServiceImpl.java
@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserMapper userMapper;
@Override
public User getUserByName(String name) {
return userMapper.getUserByName(name);
}
}
- 完善自定义的Realm类
//自定义的Realm类:继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserServiceImpl userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证");
UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
//连接数据库
User user = userService.getUserByName(userToken.getUsername());
if (user == null){//为null,没有这个人
return null;
}
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
}
补充授权:
修改ShiroConfig.java的getShiroFilterFactoryBean方法:
// Subject用户
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*anon:无需认证就可以访问
authc:必须认证了才能让问
user: 必须拥行记住我功能才能用
perms:拥有对某个资源的权限才能访问;
role:拥行某个角色权限才能访问
*/
//拦截
Map<String, String> filterMap = new LinkedHashMap<>();
//授权
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/*","perms[user:update]");
bean.setFilterChainDefinitionMap(filterMap);
//设置登录的请求
bean.setLoginUrl("/to_login");
//无权访问时,跳转的页面
bean.setUnauthorizedUrl("/noauth");
return bean;
}
同时我们自定义的Realm类中也需要加入授权:
//自定义的Realm类:继承AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserServiceImpl userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermission("user:add");
//拿到当前登录的这个对象
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();//拿到User对象
//设置当前用户的权限
info.addStringPermission(currentUser.getRoles());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("认证");
UsernamePasswordToken userToken = (UsernamePasswordToken)authenticationToken;
//连接数据库
User user = userService.getUserByName(userToken.getUsername());
if (user == null){//为null,没有这个人
return null;
}
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}