当前位置:首页 > 技术知识 > 正文内容

Spring Boot 3.X 整合 Shiro - 登录认证和权限管理

maynowei7个月前 (09-13)技术知识75

Spring Boot 3.X 整合 Shiro - 登录认证和权限管理

Apache Shiro 是一个强大且易用的 Java 安全框架,提供认证、授权、加密、会话管理等功能。本文将手把手教你在 Spring Boot 3.X 项目中整合 Shiro,实现 安全的登录认证 + 角色权限管理,并且加入生产可用的加密和缓存优化。


1. 添加依赖

在 pom.xml 中添加必要依赖:

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Shiro Spring Boot Starter -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-starter</artifactId>
        <version>2.0.0</version>
    </dependency>

    <!-- JPA + MySQL -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Lombok(可选) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

2. 数据库建表 SQL

CREATE TABLE sys_user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL UNIQUE,
  password VARCHAR(100) NOT NULL,
  salt VARCHAR(50),
  status TINYINT DEFAULT 1 COMMENT '0-锁定 1-正常'
);

CREATE TABLE sys_role (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL UNIQUE,
  description VARCHAR(100)
);

CREATE TABLE sys_permission (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50),
  permission VARCHAR(100),
  url VARCHAR(200)
);

CREATE TABLE sys_user_role (
  user_id BIGINT,
  role_id BIGINT,
  PRIMARY KEY (user_id, role_id)
);

CREATE TABLE sys_role_permission (
  role_id BIGINT,
  permission_id BIGINT,
  PRIMARY KEY (role_id, permission_id)
);

3. Shiro 核心配置

@Configuration
public class ShiroConfig {

    // 密码加密配置
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("SHA-256");
        matcher.setHashIterations(1024);
        matcher.setStoredCredentialsHexEncoded(true);
        return matcher;
    }

    // 自定义 Realm
    @Bean
    public CustomRealm customRealm() {
        CustomRealm realm = new CustomRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }

    // 安全管理器
    @Bean
    public SecurityManager securityManager(CustomRealm customRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customRealm);
        return securityManager;
    }

    // 开启 Shiro 注解支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    // Shiro 过滤器
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);

        Map<String, String> map = new LinkedHashMap<>();
        map.put("/static/**", "anon");
        map.put("/login", "anon");
        map.put("/doLogin", "anon");
        map.put("/logout", "logout");
        map.put("/**", "authc");
        factoryBean.setFilterChainDefinitionMap(map);

        factoryBean.setLoginUrl("/login");
        factoryBean.setSuccessUrl("/index");
        factoryBean.setUnauthorizedUrl("/403");
        return factoryBean;
    }
}

4. 自定义 Realm(安全版)

public class CustomRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(userService.findRoles(username));
        info.setStringPermissions(userService.findPermissions(username));
        return info;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String username = (String) token.getPrincipal();
        User user = userService.findByUsername(username);
        if (user == null) throw new UnknownAccountException("用户不存在");
        if (user.getStatus() == 0) throw new LockedAccountException("账号已锁定");

        return new SimpleAuthenticationInfo(
                user.getUsername(),
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()), // 加盐
                getName()
        );
    }
}

5. 用户服务层

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }

    public Set<String> findRoles(String username) {
        return userRepository.findRolesByUsername(username);
    }

    public Set<String> findPermissions(String username) {
        return userRepository.findPermissionsByUsername(username);
    }
}

6. 登录控制器(支持错误提示)

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginPage() {
        return "login";
    }

    @PostMapping("/doLogin")
    public String doLogin(@RequestParam String username,
                         @RequestParam String password,
                         Model model) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            subject.login(token);
            return "redirect:/index";
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "用户名错误");
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密码错误");
        } catch (LockedAccountException e) {
            model.addAttribute("msg", "账号被锁定");
        } catch (AuthenticationException e) {
            model.addAttribute("msg", "认证失败");
        }
        return "login";
    }

    @GetMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/login";
    }
}

7. 权限控制示例

@Controller
public class UserController {

    @GetMapping("/index")
    public String index() {
        return "index";
    }

    @GetMapping("/admin")
    @RequiresRoles("admin")
    public String adminPage() {
        return "admin";
    }

    @GetMapping("/user/list")
    @RequiresPermissions("user:list")
    public String userList() {
        return "user/list";
    }
}

8. 前端登录页面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<div th:if="${msg}" style="color: red;" th:text="${msg}"></div>
<form action="/doLogin" method="post">
    用户名: <input type="text" name="username"><br>
    密码: <input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>
</body>
</html>

9. 生产环境优化建议

  1. 密码加密:使用 SHA-256 + 盐 + 多次迭代(已在本文配置)。
  2. 缓存:集成 Redis 缓存角色和权限,避免频繁查库。
  3. 前后端分离:用自定义 Filter 返回 JSON,而不是跳转页面。
  4. Remember Me:支持免登录功能,提升体验。
  5. 会话管理:配置 Shiro SessionManager 或整合 Spring Session。

这样改完后,只需要导入 SQL、运行项目,就能直接体验 Spring Boot 3.x + Shiro 的登录认证和权限管理,而且是 加盐加密版 + 注解控制版 + 可扩展生产版

相关文章

Android监听滚动视图(监听页面滚动)

Android UI Libs之Android-ObservableScrollView1. 说明Android-ObservableScrollView,顾名思义,Android上观察滚动的视图,可...

Flutter 之 ListView(flutter框架)

在 Flutter 中,ListView 可以沿一个方向(垂直或水平方向)来排列其所有子 Widget,常被用于需要展示一组连续视图元素的场景ListView 构造方法ListView:仅适用于列表中...

Linux系统编程—互斥量mutex(linux 互斥量)

##互斥量mutex前文提到,系统中如果存在资源共享,线程间存在竞争,并且没有合理的同步机制的话,会出现数据混乱的现象。为了实现同步机制,Linux中提供了多种方式,其中一种方式为互斥锁mutex(也...

Go语言进阶:时间轮(golang时间轮)

时间轮概念时间轮(Timing Wheel)是一种高效的定时任务调度数据结构,特别适合处理大量定时任务。它通过一个循环数组(轮盘)和多个槽位(buckets)来组织定时任务,每个槽位代表一个时间间隔。...

Linux系统编程:条件变量为什么要用锁

条件变量可以解决线程同步和共享资源访问的问题,条件变量是对互斥锁的补充,它允许一个线程阻塞并等待另一个线程发送的信号,当收到信号时,阻塞的线程被唤醒并试图锁定与之相关的互斥锁。具体定义如下:等待:in...

大厂 Go 编程规范(二):mutex(编程大厂是什么意思)

mutex 是golang 的互斥锁,可以保障在多协程的情况下,数据访问的安全。1、零值有效我们并不需要mutex指针mu := new(sync.Mutex) mu.Lock()直接可以使用mute...