1.shiro概念
- Shiro安全框架是Apache提供的一个强大灵活的安全框架
- Shiro安全框架提供了认证、授权、企业会话管理、加密、缓存管理相关的功能
2.shiro架构
- Subject:应用代码直接交互的对象是Subject,与Subject的所有交互都会委托给SecurityManage,Subject是一个门面,SecurityManage才是执行者
- SecurityManage:安全管理器,用来处理所有有关安全的操作,并且管理所有的Subject,它负责与Subject交互,相当于SpringMvc里的Dispatchservlet
- Realm: Realm即领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库,那么realm就需要从数据库获取用户身份信息。在realm中还有认证授权校验的相关的代码
3.认证功能
项目目录如下:
注意:springboot的版本用2.x.x的,用最新的版本,项目可能跑不出来,如果有大佬看见了,并且用3.x.x的版本跑出来了,还请留言,麻烦告知下本菜鸡解决方案>_<
3.1.引入依赖
导入的依赖有:
- springboot整合shiro
- thymeleaf启动器
- springboot启动器
<!--shiro整合springboot-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<!--thymeleaf启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
3.2.编写页面和controller层
index.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<a th:href="@{/user/add}" style="color: red">add</a> | <a th:href="@{user/update}" style="color: red">update</a>
</body>
</html>
login.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${msg}"></p>
<form th:action="@{/login}">
<input type="text" name="username">
<input type="text" name="password">
<input type="submit">
</form>
</body>
</html>
controller层:
@Controller
public class MyController {
@RequestMapping("/index")
public String toIndex(Model model){
model.addAttribute("msg","hello shiro");
return "index";
}
@RequestMapping("/user/add")
public String toAdd(){
return "user/add";
}
@RequestMapping("/user/update")
public String toUpdate(){
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
3.3.创建Realm
根据上面的架构可知,要想使用Shiro,我们得先创建一个Realm,如下:
package com.kuang.config;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
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("执行了-->认证");
return null;
}
}
3.4.创建SecurityManage和ShiroFilterFactoryBean
紧接着我们来创建SecurityManage和ShiroFilterFactoryBean,如下:
- 这里的ShiroFilterFactoryBean是用来创建ShiroFilter的,它可以创建Shiro的过滤器链,然后打包交给ShiroFilter,并且将其交给Servlet,实现页面过滤和拦截功能
- 下面的代码在ShiroFilterFactoryBean这个类中,实现了过滤器的功能
package com.kuang.config;
import com.kuang.config.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
Map<String, String> FilterMap = new HashMap<>();
FilterMap.put("/user/add","authc");
FilterMap.put("/user/update","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(FilterMap);
return shiroFilterFactoryBean;
}
//DefaultWebSecurityManage
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//这个方法的参数需要一个realm类型的数据,我们就用自己定义的userRealm
//怎么样使用自定义的这个userRealm呢?
//既然我们自定义的userRealm已经交给spring托管了
//那么我们只需要让参数注入到这个userRealm的bean中即可
securityManager.setRealm(userRealm);
return securityManager;
}
//创建realm对象(需要自定义)
//这样,我们自己写的这个类就被spring托管了
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
3.5.实现用户认证
在controller包下实现这个功能,当用户登录时,若密码或者用户名不正确,则会跳转对应的页面
@RequestMapping("/login")
public String login(String username,String password,Model model){
System.out.println("123456");
//获取当前用户对象
Subject subject = SecurityUtils.getSubject();
//封装用户登录的数据并交给令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//执行登陆方法,如果没有异常就说明ok
subject.login(token);
return "index";
} catch (UnknownAccountException e) {//这个异常表明用户名不存在
model.addAttribute("msg","用户名不存在");
return "login";
}catch (IncorrectCredentialsException e){ //这个异常表示密码不存在
model.addAttribute("msg","密码不存在");
return "login";
}
}
- 上面的方法,接收了前端传来的参数,username和password
- 上面的方法有几个重要的代码:
Subject subject = SecurityUtils.getSubject(); 获取subject用户只有获取了subject才能实现shiro的相关功能 UsernamePasswordToken token = new UsernamePasswordToken(username, password);将获取的username和password交给token令牌(令牌,顾名思义就是存有用户数据的一个集合)
3.6.代码执行细节
当程序执行了上面的用户认证功能之后,shiro会把从token里获取到的数据同存放在数据库的用户数据进行对比,那么从哪里获取存放在数据库里的用户数据呢?答案是,我们自己创建的Realm
4.shiro整合mybatis(注意依赖版本)
4.1.引入依赖
上面说了,如果要让shiro和数据库里的数据进行比对,则就要使用数据库,那么就要导入相关的依赖
- springboot整合mybatis
- mysql驱动
- 数据库驱动
- lombok
<!--整合mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
注意:我用的springboot的版本是2.7.18的,所以springboot整合mybatis的依赖的版本也比较低是2.3.2的
4.2.编写查询用户的业务代码
项目目录:
4.2.1.编写pojo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
4.2.2.UserMapper
//这个注解表示了这是一个mybatis的mapper类
@Mapper
//这个注解表明将这个类注入到了spring中
@Repository
public interface UserMapper {
List<User> selectAllUser();
User selectUserByName(String name);
}
4.2.3.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">
<mapper namespace="com.kuang.mapper.UserMapper">
<select id="selectAllUser" resultType="user">
select * from tb_user
</select>
<select id="selectUserByName" parameterType="String" resultType="user">
select * from tb_user where name = #{name}
</select>
</mapper>
4.2.4.Service层
public interface UserService {
List<User> selectAllUser();
User selectUserByName(String name);
}
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userMapper;
@Override
public List<User> selectAllUser() {
return userMapper.selectAllUser();
}
@Override
public User selectUserByName(String name) {
return userMapper.selectUserByName(name);
}
}
4.2.5.测试
引入数据库之后,我们来测试一下有没有成功引入,在springboot测试类中编写
@Autowired
UserServiceImpl userService;
@Test
void contextLoads() throws SQLException {
System.out.println("11111");
System.out.println(userService.selectUserByName("小刚"));
}
5.编写自定义的Realm
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了-->认证");
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
//连接真实数据库
User user = userService.selectUserByName(usernamePasswordToken.getUsername());
if (user==null){//如果用户名为空,抛UnknownAccountException
return null;
}
//密码(对照)认证,调用SimpleAuthenticationInfo方法,shiro自动帮我们做密码认证
//返回true表明密码正确,返回false则抛出IncorrectCredentialsException异常,从MyController中就可以接收到
return new SimpleAuthenticationInfo("",user.getPwd(),"");
}
分析下上面的代码:
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken; 将参数authenticationToken类型转换成UsernamePasswordToken User user = userService.selectUserByName(usernamePasswordToken.getUsername());连接真实数据库,用usernamePasswordToken调用getUsername方法,得到token里存的从前端传来的username if (user==null){ return null; } return new SimpleAuthenticationInfo("",user.getPwd(),""); 进行判断,如果用户名为空就抛出UnknownAccountException的异常,并且返回给MyController里对应的方法
测试:
登陆页面
当用户名错误时
当密码错误时
当用户名和密码都没有错时
登陆成功,跳转到首页
6.授权功能
什么叫做授权,举个实际的例子,一个公司有普通员工,经理,总裁三个职位,当普通员工登录该公司的人员管理网站时,只能看到自己的信息,而经理不仅能看到自己的信息,还能看到他所管的部门的所有的普通员工的信息,而总裁能看到公司所有人的信息,根据不同的职位,所能进入的人员信息页面是不一样的,这就用了授权功能
6.1.在ShiroFilterFactoryBean中添加过滤器
记住:ShiroFilterFactoryBean这个类主要就是来编写过滤器的,先将指定的页面进行拦截,最后再在自定义的Realm类中进行授权
//shiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
Map<String, String> FilterMap = new HashMap<>();
//anon 无需认证就可以访问
//authc 必须认证了才能访问
//user 必须拥有记住我功能才能用
//perms 拥有对某个资源的权限才能用
//role 拥有某个角色权限才能访问
//FilterMap.put("/user/add","authc");
//FilterMap.put("/user/update","authc");
//设置授权,正常情况下未授权会跳转到未授权的页面
//必须有[user:add]才能访问/user/add这个页面
FilterMap.put("/user/add","perms[user:add]");
FilterMap.put("/user/update","perms[user:update]");
FilterMap.put("/user/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(FilterMap);
//设置未授权的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
return shiroFilterFactoryBean;
}
6.2.在Realm类中进行授权
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了-->授权");
//要给用户授权,必须先要创建SimpleAuthorizationInfo
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//为什么要创建subject,因为我们要获取用户表里的授权数据
Subject subject = SecurityUtils.getSubject();
//在认证功能中,最终会返回一个principal,而刚好有getPrincipal方法
//这样,我们在认证功能中将查询出来的用户数据交给principal,在这就能获取到
User principal =(User) subject.getPrincipal();
//设置当前用户的权限
info.addStringPermission(principal.getPerms());
//info.addStringPermission("[user:add]");
return info;
}
注意:在这个类中的getPrincipal方法,其获取的参数,要在另一个类设置好,如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了-->认证");
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
//连接真实数据库
User user = userService.selectUserByName(usernamePasswordToken.getUsername());
if (user==null){//如果用户名为空,抛UnknownAccountException
return null;
}
//密码(对照)认证,调用SimpleAuthenticationInfo方法,shiro自动帮我们做密码认证
//返回true表明密码正确,返回false则抛出IncorrectCredentialsException异常,从MyController中就可以接收到
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
6.3.设置未授权的页面
当我们没有进入某个页面的权限时,就会跳转到未授权的页面,有小伙伴可能要问了,既然没有权限,那就不要在前端显示进入这个页面的链接了呀,ok,确实如此,我们后面会实现的>_<
@RequestMapping("/noAuth")
@ResponseBody
public String unAuthorization(){
return "未授权";
}