Spring Boot 整合 Shiro 实现认证授权

发布时间:2023-03-17 13:30

文章目录

  • 一、Shiro 简介
  • 二、Spring Boot 整合 Shiro
    • 1、创建项目,导入依赖
    • 2、数据库搭建
    • 3、创建实体类
    • 4、实现 Mapper
    • 5、实现 Service
    • 6、实现 Controller
    • 7、自定义 UserRealm
    • 8、配置 Shiro
    • 9、相关页面
    • 10、认证授权测试

一、Shiro 简介

Shiro 是一个开源的轻量级的 Java 安全框架,同时支持 Java SE 和 Java EE 项目,其主要提供身份认证、授权、密码管理以及会话管理等功能。本文将介绍 Spring Boot 整合 Shiro 并实现基本的认证授权功能。

二、Spring Boot 整合 Shiro

1、创建项目,导入依赖

创建一个 Spring Boot 项目(默认添加 web 依赖),然后导入如下依赖:

		<!--shiro-->
		<dependency>
			<groupId>com.github.theborakompanioni</groupId>
			<artifactId>thymeleaf-extras-shiro</artifactId>
			<version>2.0.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.shiro</groupId>
			<artifactId>shiro-spring</artifactId>
			<version>1.4.1</version>
		</dependency>
		<!--thymeleaf-->
		<dependency>
			<groupId>org.thymeleaf</groupId>
			<artifactId>thymeleaf-spring5</artifactId>
		</dependency>
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-java8time</artifactId>
		</dependency>
		<!--数据库-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.1</version>
		</dependency>
		<!--lombok-->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

其中,shiro 的依赖为 shiro-spring ,其他的依赖是需要用到的相关组件。

2、数据库搭建

根据用户与角色的关系,简单创建如下三张表:
Spring Boot 整合 Shiro 实现认证授权_第1张图片
对应的 SQL 如下:

DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `role_id` int(10) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(30) NOT NULL,
  `role_detail` varchar(30) NOT NULL,
  PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='角色表';

insert  into `role`(`role_id`,`role_name`,`role_detail`) values 
(1,'sadmin','超级管理员'),
(2,'admin','管理员'),
(3,'user','普通用户');


DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `pwd` varchar(30) NOT NULL,
  `enabled` int(10) NOT NULL,
  `locked` int(10) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='用户表';

insert  into `user`(`id`,`name`,`pwd`,`enabled`,`locked`) values 
(1,'root','123456',1,0),
(2,'admin','123456',1,0),
(3,'study','123456',1,0);


DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `user_id` int(10) NOT NULL,
  `role_id` int(10) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='用户-角色表';

insert  into `user_role`(`id`,`user_id`,`role_id`) values 
(1,1,1),
(2,1,2),
(3,2,2),
(4,3,3);

application.yaml 中进行配置:

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/springboot-data?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  type-aliases-package: com.study.pojo
  mapper-locations: classpath:mapper/*.xml

3、创建实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
    private Integer role_id;
    private String role_name;
    private String role_detail;
}

User 中需包含一个 List,存放多个权限

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String pwd;
    private Integer enabled;
    private Integer locked;
    private List<Role> roles;
}

4、实现 Mapper

UserMapper 提供两个查询接口:根据名字查询用户;根据 id 查询用户的所有权限

@Repository
@Mapper
public interface UserMapper {

    User queryUserByName(@Param("name") String name);

    List<Role> queryRolesByUid(@Param("id") int id);

}

UserMapper.xml



<mapper namespace="com.study.mapper.UserMapper">

    <select id="queryUserByName" resultType="user">
        select * from user where name = #{name};
    select>

    <select id="queryRolesByUid" resultType="role">
        select *
        from role r, user_role ur
        where ur.role_id = r.role_id and ur.user_id = #{id};
    select>

mapper>

5、实现 Service

UserService 中封装 UserMapper 的两个查询方法:

public interface UserService {

    User queryUserByName(String name);

    List<Role> queryRolesByUid(int id);

}

实现类 UserServiceImpl

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public User queryUserByName(String name) {
        return userMapper.queryUserByName(name);
    }

    @Override
    public List<Role> queryRolesByUid(int id) {
        return userMapper.queryRolesByUid(id);
    }
}

6、实现 Controller

在 controller 中完成重定向、登录、注销等请求
重点关注的是其中的 login 方法,整个登录认证过程如下:

  1. controller 的 login 请求中接受前端传来的用户名与密码,将其封装在 UsernamePasswordToken中,然后调用 subject.login(token) 方法开启登录
  2. 然后会进入到 UserRealm(自定义) 中的 doGetAuthenticationInfo() 方法中,其中通过传入的 token 参数来获取用户输入的信息,此时在数据库中根据用户名查询用户,如果查询出的用户不为空,则返回一个 SimpleAuthenticationInfo对象,在其中将数据库中的用户信息传入,shiro 会自动地帮我们完成登录认证的功能。
  3. 如果登录认证通过,则返回至 try 语句块中;否则根据异常类型返回相应的错误信息。
@Controller
public class RouterController {

    @GetMapping("/{url}")
    public String redirect(@PathVariable("url") String url) {
        return url;
    }

    @PostMapping("/login")
    public String login(@RequestParam("username") String name, @RequestParam("password") String pwd, Model model) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(name, pwd);
        try {
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "用户名错误!");
            return "login";
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误!");
            return "login";
        }
    }

    @GetMapping("/unauth")
    @ResponseBody
    public String unauth() {
        return "未授权,无法访问!";
    }

    @GetMapping("/logout")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "login";
    }

}

7、自定义 UserRealm

UserReaml 是我们自定义的,在其中完成验证和授权的逻辑

  • 认证:执行的是 doGetAuthenticationInfo() 方法,其中根据前端输入的用户名和密码,先通过用户名查找用户,如果用户名不为空,则再将认证工作交给 shiro 实现,对应代码中的 return new SimpleAuthenticationInfo(user, user.getPwd(), getName());
  • 授权:执行的是 doGetAuthorizationInfo() 方法,我们在其中获取到当前登录的用户对象,然后在数据库中查找出其所有的权限,存入 Set 集合中,最后用其构造 SimpleAuthorizationInfo 对象并返回,即可完成授予用户权限的操作。
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Subject subject = SecurityUtils.getSubject();
        User user = (User) subject.getPrincipal();
        Set<String> roles = new HashSet<>();
        List<Role> roleList = userService.queryRolesByUid(user.getId());
        for (Role role : roleList) {
            roles.add(role.getRole_name());
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        User user = userService.queryUserByName(token.getUsername());
        if (user != null) {
            Subject subject = SecurityUtils.getSubject();
            subject.getSession().setAttribute("user", user);
            return new SimpleAuthenticationInfo(user, user.getPwd(), getName());
        }
        return null;
    }

}

8、配置 Shiro

创建 ShiroConfig 类,通过 @Configuration 开启 Shiro,不需要使用 Shiro 时直接将该注解删除即可,其中需要返回 3 个 Bean 对象,分别为 RealmDefaultWebSecurityManagerShiroFilterFactoryBean

  • Realm:其中写的是认证与授权的逻辑。即通过名称查询用户、通过 id 查询用户所拥有的角色权限,我们只需将这些信息返回给 Shiro 即可;
  • DefaultWebSecurityManager:负责接收我们传入的 Realm,然后还要将其返回并注入到最终的 ShiroFilterFactoryBean 中;
  • ShiroFilterFactoryBean:其中设置我们要拦截的请求所需要的角色权限等,比如访问哪个页面需要什么角色、权限,全都在其中定义。

常用的认证授权规则如下:

参数 含义
anon 无需认证即可访问
authc 登录后可访问
perms 拥有某个权限才可访问
role 拥有某个角色才可访问

ShiroConfig 的完整代码如下:

@Configuration
public class ShiroConfig {

    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        // 设置请求权限
        Map<String, String> map = new Hashtable<>();
        map.put("/sadmin/**", "roles[sadmin]");
        map.put("/admin/**", "roles[admin]");
        map.put("/user", "authc");
        factoryBean.setFilterChainDefinitionMap(map);
        // 设置拦截后进入的登录界面
        factoryBean.setLoginUrl("/login");
        // 设置未授权的访问失败页面
        factoryBean.setUnauthorizedUrl("/unauth");
        return factoryBean;
    }

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        return securityManager;
    }


    @Bean(name = "userRealm")
    public UserRealm getUserRealm() {
        return new UserRealm();
    }

}

代码解释:

  1. 定义一个方法并加上 @Bean 注解先将我们自定义的 UserRealm 注入到 IOC 容器中
  2. 定义一个方法返回 DefaultWebSecurityManager ,通过 setRealm方法将 1 中的 userRealm 注入到其中,同时也要加上 @Bean 注解
  3. 最后返回 ShiroFilterFactoryBean其中设置我们要拦截的请求所需要的角色权限等
    1. 首先通过 setSecurityManager 方法将 2 中的 DefaultWebSecurityManager 注入到其中;
    2. 然后将具体的请求权限配置存入一个 Map 中,key 为需授权的请求,value 为所需的角色或权限;
    3. 最后通过 setFilterChainDefinitionMap 将存放请求权限配置的 Map 添加到 Shiro 的过滤器中;
      setLoginUrl 设置拦截后进入的登录界面,setUnauthorizedUrl 设置用户权限不足导致访问失败后的跳转请求。

ps:@Bean 注解默认配置的是方法名,可通过 name 进行自定义,然后在接收参数时通过 @Qualifier() 获取

这里定义的权限如下:1. 拥有 sadmin 角色才可访问 sadmin.html;2. 拥有 admin 角色才可访问 admin.html;3. 登录后才可访问 user.html

9、相关页面

在 templates 下创建如下几个界面:
Spring Boot 整合 Shiro 实现认证授权_第2张图片
这样 controller 中的 /{url} 请求就可以根据视图名称动态返回对应的页面
其中,index.html 如下:


<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
head>
<body>
<h1>首页h1>
<p th:if="${session.user != null}">
    欢迎用户:<span th:text="${session.user.name}">span> <br>
    <a href="/logout">注销a>
p>
    <a th:href="@{/sadmin}">超级管理员入口a>
p>
<p>
    <a th:href="@{/admin}">管理员入口a>
p>
<p>
    <a th:href="@{/user}">普通用户入口a>
p>

body>
html>

10、认证授权测试

运行项目,三个超链接分别对应三种角色的请求,点击后将拦截并跳转到我们自定义的登录界面
Spring Boot 整合 Shiro 实现认证授权_第3张图片
登录用户 admin,由于其不具备 sadmin 角色,故第一张图中访问的结果是失败的;其具有 admin 角色则访问 admin 界面成功;user 界面登录后即可访问。
Spring Boot 整合 Shiro 实现认证授权_第4张图片
以上就是 Spring Boot 项目整合 Shiro 的基本用法。

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号