有道云:有道云笔记
一. 认证与授权
在学习 Shiro 框架之前,我们先来了解一下什么是认证与授权。
1.认证就是校验身份的正确性,看一下你是不是这个平台的用户。系统一般会将用户输入的账号密码和数据库的信息做比对,从而判断身份是否合法。
2.授权就是确定你是否有权访问系统的某些资源,比如查看某个页面、点击某个按钮、操作某一行数据等。
二.Shiro
Shiro 是 Apache 基金会下面一个开源的 java 权限管理框架,它可以进行身份验证、授权、密码和会话管理。
Apache Shiro | Simple. Java. Security.【官网】
【Shiro 核心架构】
对于上面复杂的架构,我们只需要记住以下几个核心模块:
1.Subject
主体,你可以理解为访问系统的用户。
2.SecurityManager
安全管理器。用户进行认证和授权都是通过 securityManager 进行,你可以理解为 shiro 的老大。
3.authenticator
认证器,用户通过 authenticator 进行认证。
4.authorizer
授权器,用户通过 authorizer 进行授权。
5.realm
领域,相当于数据源。
在 realm 中,我们通过查询数据库的信息,然后对用户进行认证和授权。
所以 authenticator 和 authorizer 其实是调用了 realm 中 认证和授权的方法。
6.cryptography
密码管理。shiro 提供了一套加密和解密的组
三.Springboot 整合 Shiro
1.新建 springboot 项目
2.引入依赖
这里我们主要引入了 web、mybatis-plus、shiro、mysql 的依赖
<!--web--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--lombok--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!--mybatis-plus--> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <!--引入 shiro 整合 Springboot 依赖--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.5.3</version> </dependency> <!--数据库驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.29</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.0</version> </dependency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity-engine-core</artifactId> <version>2.3</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
3.修改配置文件
server.servlet.context-path=/shiro server.port=8080 spring.mvc.view.suffix=.jsp spring.mvc.view.prefix=/ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql:///shiroDB spring.datasource.username=root spring.datasource.password=sasa mybatis-plus.mapper-locations=classpath*:com/lfz/mapper/*Mapper.xml mybatis-plus.type-aliases-package=com.lfz.shirodemo.entity mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
4.创建用户表
CREATE TABLE `t_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'id', `name` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '姓名', `username` VARCHAR(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '账号', `password` VARCHAR(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '密码', `salt` VARCHAR(20) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '盐', `created_date` DATETIME DEFAULT NULL COMMENT '创建时间', `updated_date` DATETIME DEFAULT NULL, `is_deleted` INT DEFAULT '0', PRIMARY KEY (`id`) USING BTREE ) ENGINE=INNODB AUTO_INCREMENT=1500052815584731139 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci ROW_FORMAT=DYNAMIC;
5.使用 mybatis-plus 生成实体类、service、dao、mapper文件
四、Shiro认证
1.文件数据源:因为 shiro 的默认数据源是配置文件数据源,所以我们先用配置文件的方式模拟数据库信息。
配置文件数据源
package com.lfz.shirodemo; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.text.IniRealm; import org.apache.shiro.subject.Subject; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ShiroDemoApplicationTests { @Test void contextLoads() { // 1.创建安全管理器 DefaultSecurityManager securityManager = new DefaultSecurityManager(); // 2.设置 realm securityManager.setRealm(new IniRealm("classpath:shiro.ini")); // 3.设置安全管理器 SecurityUtils.setSecurityManager(securityManager); // 4.获取当前主体对象 Subject subject = SecurityUtils.getSubject(); // 5.将用户登录时的账号和密码封账成 UsernamePasswordToken 对象 UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456"); try { System.out.println("认证前状态:" + subject.isAuthenticated()); subject.login(token); System.out.println("认证后状态:" + subject.isAuthenticated()); } catch (UnknownAccountException e) { System.out.println("用户不存在"); } catch (IncorrectCredentialsException e) { System.out.println("账号或密码不正确"); } } }
认证结果:
认证流程讲解:
1,你需要创建安全管理器 SecurityManager
2,为安全管理器设置数据源 Realm,Realm 的数据源默认是以配置文件的方式进行配置。
3,用户在前端输入账号和密码,shiro 框架获取当前要登录的主体(用户)对象Subject,然后将账号和密码封装成 UsernamePasswordToken 对象。
4,安全管理器通知数据源Realm 认证一下当前用户,Realm 将用户输入的账号密码信息和配置文件中的信息做比对,比对通过则认证成功,比对不通过则返回错误信息。
.2.【数据库数据】
使用数据库作为数据源需要我们自定义 Realm,其中自定义的 Realm 需要继承 AuthorizingRealm 类并实现两个方法。
并实现认证方法:
package com.lfz.shirodemo.shiro.realm; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.lfz.shirodemo.entity.User; import com.lfz.shirodemo.service.impl.UserService; import lombok.AllArgsConstructor; import lombok.Data; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; /** *使用数据库作为数据源 */ @AllArgsConstructor public class MyRealm extends AuthorizingRealm { UserService userService; /** * 授权 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } /** * 认证 * @param authenticationToken * @return */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //用户登录名称 String principal = (String) authenticationToken.getPrincipal(); User user = userService.getOne(new QueryWrapper<User>().eq("username", principal)); //用户不为空 if (null != user) { //1.账号 2.密码 3.当前 realm 名字 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),this.getName()); return simpleAuthenticationInfo; } return null; } }
测试:修改安全管理器的 Realm 为自定义的 Realm:
测试结果:
数据库添加用户信息:
重新测试:
讲解:
用户登录的账号和密码被封装成了 token 对象
调用 subject.login() 方法之后,自定义 Realm 的认证方法会接收传递的 token 信息。
其中 UsernamePasswordToken 是 AuthenticationToken 接口的实现类
getPrincipal() 方法返回的是用户的账号信息,
getCredentials() 返回的是密码信息。
将用户填写的账号和密码与数据库中的信息做比对,比对通过则返回 SimpleAuthenticationInfo 对象,其中需要传递三个参数
1.数据库中的账号
2.数据库中的密码
3.当前的 realm 对象
3.Shiro 认证完整案例
这里我们采用 shiro 配置类和登录接口的方式来实现 shiro 的认证。
1.在 UserController 中新建登录接口:
package com.lfz.shirodemo.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.stereotype.Controller; /** * <p> * 前端控制器 * </p> * * @author 悟道 */ @Controller @RequestMapping("/user") public class UserController { @RequestMapping("/login") public String login(String username, String password) { try { //获取主体对象 Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken(username, password)); return "redirect:/index.jsp"; } catch (UnknownAccountException e) { System.out.println("用户不存在"); } catch (IncorrectCredentialsException e) { System.out.println("账号或者密码错误"); } catch (Exception e) { System.out.println("服务出错"); } return "redirect:/login.jsp"; } }
2.新建自定义 Realm,实现认证方法。【上一个案例中已经实现完成,这里我们重写写一个】
package com.lfz.shirodemo.shiro.realm; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.lfz.shirodemo.entity.User; import com.lfz.shirodemo.service.impl.UserService; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.realm.Realm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; public class CustomerRealm extends AuthorizingRealm { @Autowired private UserService userService; @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //用户登录名称 String principal = (String) authenticationToken.getPrincipal(); User user = userService.getOne(new QueryWrapper<User>().eq("username", principal)); //用户不为空 if (null != user) { //1.账号 2.密码 3.当前 realm 名字 SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),this.getName()); return simpleAuthenticationInfo; } return null; } }
3.新建 shiro 的配置类
shiro 的配置类主要包含三个 bean:
- shiroFilter:用来拦截所有请求
- SecurityManager:安全管理器
- Realm:自定义 realm
完整配置类:
package com.lfz.shirodemo.shiro.config; import com.lfz.shirodemo.shiro.realm.CustomerRealm; import org.apache.shiro.realm.Realm; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.util.ThreadContext; 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; @Configuration public class ShiroConfig { /** *1.shiroFilter:负责拦截所有请求 */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManage") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 设置安全管理器 shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager); // 默认跳转页面---认证不通过时跳转 shiroFilterFactoryBean.setLoginUrl("/login.jsp"); return shiroFilterFactoryBean; } /** * 2.创建安全管理器 */ @Bean(name = "SecurityManage") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("customerRealm")Realm realm) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); ThreadContext.bind(defaultWebSecurityManager); defaultWebSecurityManager.setRealm(realm); return defaultWebSecurityManager; } /** * 创建自定义 realm */ @Bean(name="customerRealm") public Realm getRealm() { CustomerRealm customerRealm = new CustomerRealm(); return customerRealm; } }
4.发起登录请求
登录成功并跳转到首页
测试错误密码登录
登录失败,并跳转至登录页面
4.密码加密
我们经常使用 MD5 进行加密,MD5 加密是一种不可逆的加密算法。使用 MD5 加密后的结果是一个 32 位的字符串。
使用 MD5 加密需要三个参数:
- 密码
- 随机盐:其实就是一串复杂的字符串,可以使加密后的密码更复杂
- 散列次数:一般是 1024
在 Shiro 中,需要修改自定义 Realm 来完成 MD5 的加密解密:
@Bean(name="customerRealm") public Realm getRealm() { CustomerRealm customerRealm = new CustomerRealm(); //1.创建 hashed 的凭证匹配器 HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher(); //2.设置 md5 加密算法 credentialsMatcher.setHashAlgorithmName("md5"); //3.设置散列次数 credentialsMatcher.setHashIterations(1024); //4.设置 hashed 的凭证匹配器 customerRealm.setCredentialsMatcher(credentialsMatcher); return customerRealm; }
案例:
1.随机盐工具类
package com.lfz.shirodemo.shiro.util; import java.util.Random; public class SaltUtils { public static String getSalt(int n) { String str = "01234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz(#$^@%&*!)"; char[] chars = str.toCharArray(); StringBuilder randomSalt = new StringBuilder(); Random random = new Random(); for (int i = 0; i < n; i++) { int number = random.nextInt(chars.length); char aChar = chars[number]; randomSalt.append(aChar); } return randomSalt.toString(); } }
2.新增测试用户信息