shiro是apache的一个开源框架,而且呢是一个权限管理的框架,用于实现用户认证、用户授权。spring 中也有一个权限框架 spring security (原名Acegi),它和 spring 依赖过于紧密,没有 shiro 使用简单。shiro 不依赖于 spring,shiro 不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。使用shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。
New----->Project----->Spring Initializr 一路默认后就创建好一个以spring boot构建的web项目了
加入jar包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
@Test
public void authentication(){
//构建SecurityManager环境
DefaultWebSecurityManager defaultSecurityManager = new DefaultWebSecurityManager();
//创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan", "123456");
//设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
//进行登入(提交认证)
subject.login(token);
}catch (IncorrectCredentialsException exception){
System.out.println("用户名密码不匹配");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated()); //登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
上面的小 demo 是 shiro 实现认证的一个过程,做权限管理的时候,分为两块,一个认证,一个是授权,认证是判断用户是否账号密码正确,授权是判断用户登入以后有什么权限
在 junit 单元测试类创建 shiro 授权的测试方法
@Test
public void authorization(){
//构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//创建一个SimpleAccountRealm 域
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
//添加一个测试账号、和所拥有的角色(后面可以做成读取动态读取数据库)
simpleAccountRealm.addAccount("zhangsan", "123456","admin","user");
//设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码(用户输入的用户名密码)生成token
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
//进行登入(提交认证)
subject.login(token);
subject.checkRoles("admin","user");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名密码不匹配");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
//登出logout
System.out.println("执行 logout()方法后");
subject.logout();
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
}
测试
我们利用 SimpleAccountRealm 在程序中写死了用户安全数据,接下来我们使用 .ini 将数据移到配置文件中。
IniRealm是Shiro提供一种Realm实现。用户、角色、权限等信息集中在一个.ini文件那里。
在 resources 目录下创建一个 shiro.ini 文件
#账号信息
[users]
#账号=密码,角色
test=123456,test
admin=123456,admin
#角色信息
[roles]
test=user:list,user:deleted,user:edit
admin= *
在 junit 单元测试类创建 testIniRealm 方法
@Test
public void testIniRealm(){
//配置文件中的用户权限信息,文件在类路径下
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("用户认证的状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("test");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:list");
System.out.println("有user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
当用户 test 进入到程序时候我们首先用 IniRealm 读取 shiro.ini 配置获得 test 用户的一些安全信息。
假如是用户 admin 登录进来呢?
上一节课我们主要讲了把用户安全信息(相应的角色/权限)配置在 .ini 文件,使用 IniRealm 去读取 .ini 文件获得用户的安全信息。也是有局限性的。因为我们得事先把所有用户信息配置在.ini 文件,这样显然是行不通的,我们的系统用户都是动态的不固定的,它的一些用户信息权限信息都是变化的,所以固定在.ini 配置文件显然是行不通的。这些数据通常我们都是把它存入到DB 中,那shiro 有没有提供直接从DB读取用户安全信息的域呢 ?
很明显看到 jdbc 就明白是跟数据有关的吧,所以我们要引入 mysql 数据库驱动
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
有了mysql数据库驱动是不是得加个数据源呀?这里我们用阿里的druid
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
下面我们创建一个名为 shiro 的数据库、分别创建三张表 users、user_roles、roles_permissions
CREATE DATABASE IF NOT EXISTS shiro DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
users
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(25) DEFAULT NULL,
`password` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
user_roles
CREATE TABLE `user_roles` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`role_name` varchar(25) DEFAULT NULL,
`username` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
roles_permissions
CREATE TABLE `roles_permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`permission` varchar(255) DEFAULT NULL,
`role_name` varchar(25) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
分别插入如下几条数据
USE shiro;
INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('1', 'admin', '123456'); INSERT INTO `shiro`.`users` (`id`, `username`, `password`) VALUES ('2', 'test', '123456'); INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('1', 'admin', 'admin'); INSERT INTO `shiro`.`user_roles` (`id`, `role_name`, `username`) VALUES ('2', 'test', 'test'); INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('1', 'user:deleted', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('2', 'user:list', 'test');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('3', '*','admin');
INSERT INTO `shiro`.`roles_permissions` (`id`, `permission`, `role name`) VALUES ('4', 'user:edit', 'test');
@Test
public void testJdbcRealm(){
//配置数据源
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
//配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//使用JdbcRealm下面的值需要为true不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("认证状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete");
System.out.println("有user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
USE shiro;
ALTER TABLE users RENAME sys_users;
ALTER TABLE user_roles RENAME sys_user_roles;
ALTER TABLE roles_permissions RENAME sys_roles_permissions;
@Test
public void testNewJdbcRealm(){
//配置数据源
DruidDataSource dataSource = new DruidDataSource();
//如果使用的是新版的驱动 配置driver的时候要注意
dataSource.setUrl("jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC");
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUsername("root");
dataSource.setPassword("root");
//配置文件中的用户权限信息,文件在类路径下
JdbcRealm jdbcRealm = new JdbcRealm();
jdbcRealm.setDataSource(dataSource);
//使用JdbcRealm下面的值需要为true不然无法查询用户权限
jdbcRealm.setPermissionsLookupEnabled(true);
//使用自定义sql查询用户信息
String sql="select password from sys users where username = ?"; jdbcRealm.setAuthenticationQuery(sql);
String roleSql = "select role name from sys_user_roles where username = ?"; jdbcRealm.setUserRolesQuery(roleSql);
String permissionsSql = "select permission from sys_roles_permissions where role_name = ?"; jdbcRealm.setPermissionsQuery(permissionsSql);
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(jdbcRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("认证状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add");
System.out.println("有 user:add、user:list、user:edit、user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
虽然 jdbcRealm 已经实现了从数据库中获取用户的验证信息,但是 jdbcRealm灵活性也是稍差一些的,如果要实现自己的一些特殊应用时将不能支持,这个时候可以通过自定义realm来实现身份的认证功能。
通常自定义Realm只需要继承:AuthorizingRealm重写 doGetAuthenticationInfo(用户认证)、doGetAuthorizationInfo(用户授权) 这两个方法即可。
package com.yingxue.lesson.shiro;
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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class CustomRealm extends AuthorizingRealm {
// 模拟数据库中的用户名和密码
private Map<String, String> userMap =new HashMap<>();
// 使用代码块初始化数据
{
userMap.put("admin", "123456");
userMap.put("test","123456");
}
/**
* 用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //这个就是SimpleAuthenticationInfo(username,password,getName()); 第一个参数
String username= (String) SecurityUtils.getSubject().getPrincipal(); System.out.println("开始执行*******doGetAuthorizationInfo方法啦****"); System.out.println(SecurityUtils.getSubject().getPrincipal()); String username = (String) principalCollection.getPrimaryPrincipal();
//从数据库或者缓存中获取角色数据
List<String> roles = getRolesByUsername(username);
//从数据库或者缓存中获取权限数据
List<String> permissions = getPerminssionsByUsername(username); //创建AuthorizationInfo,并设置角色和权限信息
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addStringPermissions(permissions); authorizationInfo.addRoles(roles);
return authorizationInfo;
}
/**
* 用户人证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始执行*******doGetAuthenticationInfo方法啦****");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
return authenticationInfo;
}
/**
* 模拟通过数据库获取权限数据
*/
private List<String> getPerminssionsByUsername(String username) { List<String> permissions = new ArrayList<>();
// 只有是 admin 用户才有 新增、删除权限
if(username.equals("admin")){
permissions.add("user:delete");
permissions.add("user:add");
}
permissions.add("user:edit");
permissions.add("user:list");
return permissions;
}
/**
* 模拟通过数据库获取用户角色信息
*/
private List<String> getRolesByUsername(String username) { List<String> roles = new ArrayList<>(); if(username.equals("admin")){
roles.add("admin");
}
roles.add("test");
return roles;
}
/**
* 通过用户名查询密码,模拟数据库查询
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
}
@Test
public void testCustomRealm(){
CustomRealm customRealm=new CustomRealm();
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(customRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("认证状态:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add"); System.out.println("有 user:add、user:list、user:edit、user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
1、当用户 admin 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
2、当用户 test 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("test", "123456");
3、当用户 dev 登陆进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("dev", "123456");
通过上述验证过程我们可以发现,shiro 更多的是帮助我们完成验证过程。我们需要从数据库查询当前用户的角色、权限,把这些信息告诉 shiro 框架。当我们执行用户认证的时候首先调用 doGetAuthenticationInfo 进行用户认证,当我们要校验权限的时候 就会执行doGetAuthorizationInfo 进行授权操作。
自定义 Realm,里面的用户认证所使用的密码都是明文,这种方式是不可取的往往我们在实战开发中用户的密码都是以密文形势进行存储,并且要求加密算法是不可逆的,著名的加密算法有MD5、SHA1等。所以这节课我们主要介绍 Shiro 安全框架学习它的加密方案。这里我们采用md5的方式加密,然后呢。md5加密又分为加盐,和不加盐。
@Test
public void testMatcher(){
CustomRealm customRealm=new CustomRealm();
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(customRealm); SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("dev", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("是否权限认证:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add"); System.out.println("有 user:add、user:list、user:edit、user:delete权限");
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
//进行加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//采用md5加密
matcher.setHashAlgorithmName("md5");
//设置加密次数
matcher.setHashIterations(2);
customRealm.setCredentialsMatcher(matcher);
/**
* 获得密文密码
*/
private String getPasswordMatcher(String currentPassword){
return new Md5Hash(currentPassword, null,2).toString();
}
/**
* 用户人证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始执行*******doGetAuthenticationInfo方法啦****");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
String matcherPwd=getPasswordMatcher(password);
System.out.println(matcherPwd);
SimpleAuthenticationInfo authenticationInfo = new
SimpleAuthenticationInfo(username,matcherPwd,getName()); return authenticationInfo;
}
1、当用户 admin 登录进来的时候 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
当两个用户的密码相同时,单纯使用不加盐的MD5加密方式,会发现数据库中存在相同结构的密码,这样也是不安全的。我们希望即便是两个人的原始密码一样,加密后的结果也不一样。如何做到呢?其实就好像炒菜一样,两道一样的鱼香肉丝,加的盐不一样,炒出来的味道就不一样。MD5加密也是一样,需要进行盐值加密。
@Test
public void testSaltMatcher(){
CustomRealm customRealm=new CustomRealm();
//进行加密
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
//采用mdf加密
matcher.setHashAlgorithmName("md5");
//设置加密次数
matcher.setHashIterations(1);
customRealm.setCredentialsMatcher(matcher);
//1,构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
//设置Realm
defaultSecurityManager.setRealm(customRealm); SecurityUtils.setSecurityManager(defaultSecurityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//用户名和密码的token
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
try {
//2,主体提交认证请求
subject.login(token);
System.out.println("是否权限认证:isAuthenticated=" + subject.isAuthenticated());
//检查是否有角色
subject.checkRoles("admin");
System.out.println("有admin角色");
//检查是否有权限
subject.checkPermissions("user:delete","user:edit","user:list","user:add"); System.out.println("有 user:add、user:list、user:edit、user:delete权限"); //if no exception, that's it, we're done!
}catch (IncorrectCredentialsException exception){
System.out.println("用户名或密码错误");
}catch (LockedAccountException exception){
System.out.println("账号已被锁定");
}catch (DisabledAccountException exception){
System.out.println("账号已被禁用");
}catch (UnknownAccountException exception){
System.out.println("用户不存在");
}catch ( UnauthorizedException ae ) {
System.out.println("用户没有权限");
}
}
//设置盐的值
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
/**
* 获得密文密码
*/
private String getPasswordMatcher(String currentPassword,String username){
return new Md5Hash(currentPassword, username).toString();
}
/**
* 用户人证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("开始执行*******doGetAuthenticationInfo方法啦****");
//获取登录用户名
String username = (String) authenticationToken.getPrincipal();
//通过用户名到数据库获取用户信息
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
// String matcherPwd=getPasswordMatcher(password);
// System.out.println(matcherPwd);
// SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,matcherPwd,getName());
// SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,getName());
// return authenticationInfo;
String matcherPwd=getPasswordMatcher(password,username);
System.out.println(matcherPwd); SimpleAuthenticationInfo authenticationInfo = new
SimpleAuthenticationInfo(username,matcherPwd,getName());
authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes(username));
return authenticationInfo;
}
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456");
2、当用户 admin 登录但是密码却输成1234567 程序输出
UsernamePasswordToken token = new UsernamePasswordToken("admin", "1234567");
3、当用户 admin 登录进来密码也输入正确了但是 验证的时候改用明文 程序输出
//把下面两个方法注释掉
// String matcherPwd=getPasswordMatcher(password);
// System.out.println(matcherPwd);
SimpleAuthenticationInfo authenticationInfo = new
SimpleAuthenticationInfo(username,password,getName());
shiro 加密就讲到这里,其实有的同学会有疑问,假如我们公司用 sessionID 作为登录状态的凭证那么我们该怎么办呢,怎么才能让用户通过认证呢?这里我们可以自定义密码匹配类继承 HashedCredentialsMatcher类 重写它的 doCredentialsMatch (密码匹配的方法)即可。后面我们讲解实战开发的时候会详细的讲解。
需求:
响应码约定:
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--shiro 相关-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!--redis 相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger 相关-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-
4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.10.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yingxue.lesson</groupId>
<artifactId>shiro-combat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro-combat</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>
<!--shiro 相关-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!--redis 相关-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger 相关-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!--数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
package com.yingxue.lesson.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
import java.util.List;
@EnableSwagger2
@Configuration
public class SwaggerConfig {
@Value("${swagger2.enable}")
private boolean enable;
@Bean
public Docket createRestApi() {
// 这是为了我们在用 swagger 测试接口的时候添加头部信息
List<Parameter> pars = new ArrayList<Parameter>();
ParameterBuilder tokenPar = new ParameterBuilder();
tokenPar.name("sessionId").description("swagger测试用(模拟sessionId传入)非必填header").modelRef(new ModelRef("string")).parameterType("header").required(false);
// 多个的时候 就直接添加到 pars 就可以了
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.yingxue.lesson.controller"))
.paths(PathSelectors.any())
.build()
.globalOperationParameters(pars)
.enable(enable);
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("迎学教育--shiro 实战")
.description("迎学教育-spring boot 实战系列")
.termsOfServiceUrl("")
.version("1.0")
.build();
}
}
#swagger 开关
swagger2.enable=true
package com.yingxue.lesson.serializer;
import com.alibaba.fastjson.JSON;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class MyStringRedisSerializer implements RedisSerializer<Object> {
private final Charset charset;
public MyStringRedisSerializer() {
this(StandardCharsets.UTF_8);
}
public MyStringRedisSerializer(Charset charset) { Assert.notNull(charset, "Charset must not be null!"); this.charset = charset;
}
@Override
public String deserialize(byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(Object object) {
if (object == null) {
return new byte[0];
}
if(object instanceof String){
return object.toString().getBytes(charset);
}else {
String string = JSON.toJSONString(object);
return string.getBytes(charset);
}
}
}
package com.yingxue.lesson.config;
import com.yingxue.lesson.serializer.MyStringRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
MyStringRedisSerializer myStringRedisSerializer=new MyStringRedisSerializer();
StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();
redisTemplate.setValueSerializer(myStringRedisSerializer);
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(myStringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
return redisTemplate;
}
}
package com.yingxue.lesson.exception;
public class BusinessException extends RuntimeException{
// 异常编号
private final int messageCode;
// 对messageCode 异常信息进行补充说明
private final String detailMessage;
public BusinessException(int messageCode, String message) {
super(message);
this.messageCode = messageCode;
this.detailMessage = message;
}
public int getMessageCode() {
return messageCode;
}
public String getDetailMessage() {
return detailMessage;
}
}
# Redis 服务器地址
spring.redis.host=localhost
# Redis 服务?连接端?
spring.redis.port=6379
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=100
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=PT10S
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=30
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=1
# 链接超时时间
spring.redis.timeout=PT10S
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"> <generatorConfiguration>
<!--classPathEntry:数据库的JDBC驱动,换成你自己的驱动位置 -->
<classPathEntry location="F:\mvnrepository\mysql\mysql-connector-java\5.1.28\mysql-connector-java-5.1.28.jar" />
<!-- 一个数据库一个context -->
<!--defaultModelType="flat" 大数据字段,不分表 -->
<context id="MysqlTables" targetRuntime="MyBatis3" defaultModelType="flat"> <property name="autoDelimitKeywords" value="true"/>
<property name="beginningDelimiter" value="`"/>
" value=" `"/>
<property name="javaFileEncoding" value="utf-8"/>
<plugin type="org.mybatis.generator.plugins.SerializablePlugin"/>
<plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
<!-- 注释 -->
<commentGenerator>
<!-- 是否取消注释 -->
<property name="suppressAllComments" value="true"/>
<!-- 是否生成注释代时间戳-->
<property name="suppressDate" value="true"/>
</commentGenerator>
<!-- jdbc连接 -->
<jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql://localhost:3306/test_mybatis" userId="root"
password="root"/>
<!-- 类型转换 -->
<javaTypeResolver>
<!-- 是否使用bigDecimal, false可自动转化以下类型(Long, Integer, Short, etc.) -->
<property name="forceBigDecimals" value="false"/>
</javaTypeResolver>
<!-- 生成实体类地址H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\java (要改成你自己实际的目录) -->
<javaModelGenerator targetPackage="com.yingxue.lesson.entity"
targetProject="H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\java">
<property name="enableSubPackages" value="false"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!-- 生成mapxml文件 H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\resources (要改成你自己实际的目录) -->
<sqlMapGenerator targetPackage="mapper" targetProject="H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\resources">
<property name="enableSubPackages" value="false"/>
</sqlMapGenerator>
<!-- 生成mapxml对应client,也就是接口dao -->
<javaClientGenerator targetPackage="com.yingxue.lesson.mapper" targetProject="H:\Business\Spring Boot\code\springboot4\mybatis-demo\yingxue-dao\src\main\java"
type="XMLMAPPER">
<property name="enableSubPackages" value="false"/>
</javaClientGenerator>
<table tableName="sys_user" domainObjectName="SysUser"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"
enableSelectByExample="false"
selectByExampleQueryId="true">
<columnOverride column="sex" javaType="java.lang.Integer"/>
<columnOverride column="status" javaType="java.lang.Integer"/>
<columnOverride column="create_where" javaType="java.lang.Integer"/>
<columnOverride column="deleted" javaType="java.lang.Integer"/>
</table>
</context>
</generatorConfiguration>
<!--配置mybatis代码生成工具-->
<!--使用生成工具可以直接使用maven的命令提示符,
生成语句是mvn mybatis-generator:generate,
一旦数据库进行了更改,
都需使用这句代码重新生成bean、dao、mapper文件-->
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.5</version>
<configuration>
<configurationFile>
src/main/resources/generatorConfig.xml
</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<executions>
<execution>
<phase>deploy</phase>
<id>Generate MyBatis Artifacts</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.5</version>
</dependency>
</dependencies>
</plugin>
#数据库配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver spring.datasource.druid.url=jdbc:mysql://localhost:3306/test_mybatis? useUnicode=true&characterEncoding=utf-8&useSSL=false spring.datasource.druid.username=root spring.datasource.druid.password=root
################## 连接池配置 ################
#连接池建立时创建的初始化连接数
spring.datasource.druid.initial-size=5
#连接池中最大的活跃连接数
spring.datasource.druid.max-active=20
#连接池中最小的活跃连接数
spring.datasource.druid.min-idle=5
# 配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.druid.pool-prepared-statements=true spring.datasource.druid.max-pool-prepared-statement-per-connection-size=20 spring.datasource.druid.validation-query=SELECT 1 FROM DUAL spring.datasource.druid.validation-query-timeout=30000
#是否在获得连接后检测其可用性
spring.datasource.druid.test-on-borrow=false
#是否在连接放回连接池后检测其可用性
spring.datasource.druid.test-on-return=false
#是否在连接空闲一段时间后检测其可用性
spring.datasource.druid.test-while-idle=true
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
mybatis.mapper-locations=classpath:mapper/*.xml
@MapperScan("com.yingxue.lesson.mapper")
package com.yingxue.lesson.shiro;
import org.apache.shiro.authc.UsernamePasswordToken;
public class CustomPasswordToken extends UsernamePasswordToken {
private String token;
public CustomPasswordToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
}
package com.yingxue.lesson.shiro;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONPObject;
import com.yingxue.lesson.constants.Constant;
import com.yingxue.lesson.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class CustomAccessControlFilter extends AccessControlFilter {
/**
* 是否允许访问
* true:允许,交下一个Filter处理
* false:回往下执行onAccessDenied
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
return false;
}
/**
* 表示访问拒绝时是否自己处理,
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException, ServletException {
HttpServletRequest request= (HttpServletRequest) servletRequest;
try {
log.info(request.getMethod());
log.info(request.getRequestURL().toString());
//获取用户凭证
String accessToken=request.getHeader(Constant.SESSION ID); if(StringUtils.isEmpty(accessToken)){
throw new BusinessException(4001002,"用户凭证为空请重新登录");
}
CustomPasswordToken customPasswordToken=new CustomPasswordToken(accessToken);
// 委托给Realm进行登录
getSubject(servletRequest, servletResponse).login(customPasswordToken); }catch (BusinessException e) {
customResponse(e.getMessageCode(),e.getDefaultMessage(),servletResponse); return false;
} catch (AuthenticationException e) {
if(e.getCause() instanceof BusinessException){
BusinessException exception= (BusinessException) e.getCause();
customResponse(exception.getMessageCode(),exception.getDefaultMessage(),servletResponse);
return false;
}else {
customResponse(4000001,"用户认证失败",servletResponse);
return false;
}
}catch (AuthorizationException e){
if(e.getCause() instanceof BusinessException){
BusinessException exception= (BusinessException) e.getCause();
customResponse(exception.getMessageCode(),exception.getDefaultMessage(),servletResponse); return false;
}else {
customResponse(4030001,"没有访问的权限",servletResponse);
return false;
}
}
catch (Exception e){
if(e.getCause() instanceof BusinessException){
BusinessException exception= (BusinessException) e.getCause();
customResponse(exception.getMessageCode(),exception.getDefaultMessage(),servletResponse); return false;
}else {
customResponse(5000001,"系统异常",servletResponse);
return false;
}
}
return true;
}
/**
* 自定义错误响应
*/
private void customRsponse(int code, String msg, ServletResponse response){
// 自定义异常的类,用户返回给客户端相应的JSON格式的信息 try {
Map<String,Object> result=new HashMap<>(); result.put("code",code); result.put("msg",msg);
response.setContentType("application/json; charset=utf-8"); response.setCharacterEncoding("UTF-8");
String userJson = JSON.toJSONString(result);
OutputStream out = response.getOutputStream();
out.write(userJson.getBytes("UTF-8"));
out.flush();
} catch (IOException e) { log.error("eror={}",e);
}
}
}
package com.yingxue.lesson.shiro;
import com.yingxue.lesson.exception.BusinessException;
import com.yingxue.lesson.service.RedisService;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; import org.springframework.beans.factory.annotation.Autowired;
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher {
@Autowired
private RedisService redisService;
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
CustomPasswordToken customPasswordToken= (CustomPasswordToken) token;
String accessToken = (String) customPasswordToken.getPrincipal();
if(!redisService.hasKey(accessToken)){
throw new BusinessException(4001002,"授权信息信息无效请重新登录");
}
return true;
}
}
package com.yingxue.lesson.shiro;
import com.yingxue.lesson.service.RedisService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
@Slf4j
public class CustomRealm extends AuthorizingRealm {
@Autowired
private RedisService redisService;
/**
* 设置支持令牌校验
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof CustomPasswordToken;
}
/**
* 主要业务:
* 系统业务出现要验证用户的角色权限的时候,就会调用这个方法
* 来获取该用户所拥有的角色/权限
* 这个用户授权的方法我们可以缓存起来不用每次都调用这个方法。
* 后续的课程我们会结合 redis 实现它
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
String accessToken= (String) SecurityUtils.getSubject().getPrincipal();
String userId= (String) redisService.get(accessToken);
authorizationInfo.addRoles(getRolesByUserId(userId));
authorizationInfo.setStringPermissions(getPermissionByUserId(userId));
return authorizationInfo;
}
/**
* 主要业务:
* 当业务代码调用 subject.login(customPasswordToken); 方法后
* 就会自动调用这个方法 验证用户名/密码
* 这里我们改造成 验证 token 是否有效 已经自定义了 shiro 验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
CustomPasswordToken token= (CustomPasswordToken) authenticationToken;
SimpleAuthenticationInfo simpleAuthenticationInfo=new
SimpleAuthenticationInfo(token.getPrincipal(),token.getPrincipal(),getName());
return simpleAuthenticationInfo;
}
/**
* 获取用户的角色
* 这里先用伪代码代替
* 后面我们讲到权限管理系统后 再从 DB 读取
*/
private List<String> getRolesByUserId(String userId) {
List<String> roles = new ArrayList<>();
if(userId.equals("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8")){
roles.add("admin");
}else {
roles.add("test");
}
return roles;
}
/**
* 获取用户的权限
* 这里先用伪代码代替
* 后面我们讲到权限管理系统后 再从 DB 读取
*/
private List<String> getPermissionByUserId(String userId) {
List<String> permissions = new ArrayList<>();
//只有是 admin 用户才拥有所有权限
if(userId.equals("9a26f5f1-cbd2-473d-82db-1d6dcf4598f8")){
permissions.add("*");
}else {
permissions.add("sys:user:edit");
permissions.add("sys:user:list");
}
return permissions;
}
}
package com.yingxue.lesson.config;
import com.yingxue.lesson.shiro.CustomAccessControlFilter;
import com.yingxue.lesson.shiro.CustomHashedCredentialsMatcher;
import com.yingxue.lesson.shiro.CustomRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
/**
* 自定义密码 校验
*/
@Bean
public CustomHashedCredentialsMatcher customHashedCredentialsMatcher(){ return new CustomHashedCredentialsMatcher();
}
/**
* 自定义域
*/
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm=new CustomRealm();
customRealm.setCredentialsMatcher(customHashedCredentialsMatcher());
return customRealm;
}
/**
* 安全管理
*/
@Bean
public SecurityManager securityManager(){
//构建 SecurityManager环境
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//自定义 Realm
securityManager.setRealm(customRealm());
return securityManager;
}
/**
* shiro过滤器,配置拦截哪些请求
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//自定义拦截器限制并发人数,参考博客:
LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
//用来校验token
filtersMap.put("token", new CustomAccessControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/api/user/login", "anon");
//放开swagger-ui地址
filterChainDefinitionMap.put("/swagger/**", "anon");
filterChainDefinitionMap.put("/v2/api-docs", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
filterChainDefinitionMap.put("/swagger-resources/**", "anon");
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
filterChainDefinitionMap.put("/captcha.jpg", "anon");
filterChainDefinitionMap.put("/csrf","anon");
filterChainDefinitionMap.put("/**","token,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new
DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
}
@Override
public LoginRespVO login(LoginReqVO vo) {
SysUser userByName = sysUserMapper.getUserByName(vo.getUsername()); if(userByName==null){
throw new BusinessException(4001004,"用户名密码不匹配");
}
if(userByName.getStatus()==2){
throw new BusinessException(4001004,"该账户已经被锁定,请联系系统管理员");
}
if(!PasswordUtils.matches(userByName.getSalt(),vo.getPassword(),userByName.getPassword())){
throw new BusinessException(4001004,"用户名密码不匹配");
}
LoginRespVO loginRespVO=new LoginRespVO();
loginRespVO.setId(userByName.getId());
String token= UUID.randomUUID().toString();
loginRespVO.setToken(token);
redisService.set(token,userByName.getId(),60, TimeUnit.MINUTES);
return loginRespVO;
}
/**
* 获得密文密码
*/
private String getPasswordMatcher(String currentPassword,String salt){ return new Md5Hash(currentPassword, salt).toString();
}
@Override
public SysUser detail(String id) {
return sysUserMapper.selectByPrimaryKey(id);
}
package com.yingxue.lesson.controller;
import com.yingxue.lesson.entity.SysUser;
import com.yingxue.lesson.service.UserService;
import com.yingxue.lesson.vo.req.LoginReqVO;
import com.yingxue.lesson.vo.resp.LoginRespVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
@Api(tags = "用户模块")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/user/login")
@ApiOperation(value ="用户登录接口")
public Map<String, Object> login(@RequestBody LoginReqVO vo){
Map<String,Object> result=new HashMap<>();
result.put("code",0);
result.put("data",userService.login(vo));
return result;
}
@GetMapping("/user/{id}")
@ApiModelProperty(value = "查询用户详情接口")
@RequiresPermissions("sys:user:detail")
public Map<String, Object> detail(@PathVariable("id") @ApiParam(value = "用户Id") String id){
Map<String,Object> result=new HashMap<>();
result.put("code",0);
result.put("data",userService.detail(id));
return result;
}
}
filterChainDefinitionMap.put("/api/user/login", "anon");
1、用 LoginReqVO 接收用户提交过来的用户名密码的数据
2、把 vo 传入业务层接口进行业务处理
3、登录业务处理
这个接口有两个关键点,因为用户首次登录后,后续再访问我们的系统资源的时候,无需再传入用户密码进行验证只需要携带登录生成的token可以了,我们的后端会围绕token使用shiro进行一系列的认证。当用户通过了用户认证的时候还需要进行授权,因为用户详解接口设置了访问权限(@RequiresPermissions(“sys:user:detail”))所以我们还要对访问的用户进行授权。