个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道
如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充
前言
权限管理对于一个成熟的项目是极为基础且必要的部分,我们必须知道来者何人,才能判断这个人能做什么不能做什么
而shiro正是解决方案中最常见的一种
请留意,本文主要整理shiro相关思路,为方便理解,示例代码并不完整,更谈不上严谨
若需要实际使用的demo,请直接查看整理后的代码,完整demo会传到github或码云
1.介绍
- shiro是Apache下的一个开源项目,提供认证、授权、加密和会话管理等功能
- shiro属于轻量级框架,相当于SpringSecurity的精简版,更加轻便,当然内容也更少,不过足以应付大量场景
2.结构
shiro的三大核心组件为Subject、SecurityManager 和 Realm
2.1.Subject:认证主体
包括两个信息,便可辨认出需要认证的身份
-
Principals:身份。用于标识登录主体,通常为用户名、手机号等等
-
Credentials:凭证。用于验证主体身份,通常为密码、数字证书等等
2.2.SecurityManager:安全管理器
为shiro的核心,负责管理所有的subject和与之先关的交互操作
2.3.Realm:数据域
即数据来源,shiro会从这里回去安全数据,用于验证身份和分配权限,通常使用缓存或者数据库来实现(数据库和redis都是常见的实现方案)
可以看出,shiro并不提供和维护安全数据,仅仅是进行验证身份和分配权限,需要开发者去维护安全数据
3.细节功能
-
Authentication:身份认证/登录(账号密码验证)。
-
Session Manager:会话管理,用户登录后的session相关管理。
-
Cryptography:加密,密码加密等。
-
Web Support:Web支持,集成Web环境。
-
Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。
-
Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。
-
Testing:测试支持;
-
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。
-
Remember Me:记住我,登录后,下次再来的话不用登录了。
4.主要业务
4.1.拦截
拦截器会拦截请求,并根据配置,决定下一步业务是拒绝访问、重定向或进入控制层/业务层
4.2.登录
流程如下:
- 控制层/业务层根据用户传递的数据创建token,即认证主体Subject
- 控制层/业务层发起登录行为
- shiro根据token中的数据,查询数据库账号相关数据,生成认证信息。没有数据则认证失败
- shiro将认证信息与认证主体进行匹配,不匹配则认证失败
- shiro通过认证主体中的信息,查询数据库里权限数据,并将其写入数据域保存
换言之,通过用户传递的数据生成认证主体,再通过查询数据库生成认证信息,对两者进行匹配,成功就再查询权限信息保存,不匹配就再见
4.3.身份验证
流程如下:
- 根据请求头中的token在数据域查询认证信息,未查询到则认证失败
- 对认证信息中的角色、权限或其他自定义信息进行匹配,任意一个不符合则认证失败
- 认证成功则进入控制层/业务层,且仍可通过代码控制进行身份验证和认证数据读取
也就是,通过token查询认证相关数据,没查到就是没认证
5.集成
5.1.添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
5.2.建立相关数据库表
此处建立三张表:用户表user_info、角色表role_info、权限表permission_info,并生成对应实体类
(为方便调试,仅生成实体类,并模拟查询过程)
-
UserInfo
@Data @AllArgsConstructor public class UserInfo { /** * 主键id */ private Integer id; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 外键关联role表 */ private Integer roleId; }
-
RoleInfo
@Data @AllArgsConstructor public class RoleInfo { /** * 主键id */ private Integer id; /** * 角色名称 */ private String roleName; }
-
PermissionInfo
@Data @AllArgsConstructor public class PermissionInfo { /** * 主键id */ private Integer id; /** * 权限名 */ private String permissionName; /** * 外键关联role */ private Integer roleId; }
-
查询方法(模拟查询,方便调试)
public UserInfo getUserInfo(String userName) { UserInfo userInfo; switch (userName) { case "user1": userInfo = new UserInfo(1, "user1", "pwd1", 1); break; case "user2": userInfo = new UserInfo(2, "user2", "pwd2", 2); break; case "user3": userInfo = new UserInfo(3, "user3", "pwd3", 3); break; default: userInfo = null; break; } return userInfo; } public List<PermissionInfo> getPermissionList(String userName) { List<PermissionInfo> list = new ArrayList<>(); list.add(new PermissionInfo(1, "student", 1)); list.add(new PermissionInfo(2, "teacher", 2)); list.add(new PermissionInfo(3, "teacher", 3)); list.add(new PermissionInfo(4, "student", 3)); UserInfo userInfo = getUserInfo(userName); List<PermissionInfo> result = new ArrayList<>(); for (PermissionInfo permissionInfo : list) { if (permissionInfo.getRoleId() == userInfo.getRoleId()) { result.add(permissionInfo); } } return result; }
如上述数据中
- user1拥有student权限
- user2拥有teacher权限
- user3拥有两种权限student&teacher
5.3.自定义Realm
集成AuthorizingRealm
类,重写两个方法
- doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
- doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
package com.yezi_tool.basic_project.shiro;
import com.yezi_tool.basic_project.commons.model.UserRealmInfo;
import com.yezi_tool.basic_project.entity.PermissionInfo;
import com.yezi_tool.basic_project.entity.RoleInfo;
import com.yezi_tool.basic_project.entity.UserInfo;
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 java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Echo_Ye
* @title 自定义realm
* @description 自定义数据域
* @date 2020/8/17 9:25
* @email echo_yezi@qq.com
*/
@Slf4j
public class MyRealm extends AuthorizingRealm {
/**
* 分配权限
*
* @param principalCollection 数据源
* @return 权限和角色信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("-----------------访问授权---------------------");
//认证信息未登录,可能是用户非正常退出
if (!SecurityUtils.getSubject().isAuthenticated()) {
doClearCache(principalCollection);
SecurityUtils.getSubject().logout();
return null;
}
//获取认证主体
String username = (String) principalCollection.getPrimaryPrincipal();
if (username == null) {
//认证错误
return null;
}
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<PermissionInfo> permissionInfoList = getPermissionList(username);
Set<String> permissionSet = permissionInfoList.stream().map(m -> m.getPermissionName()).collect(Collectors.toSet());
simpleAuthorizationInfo.setStringPermissions(permissionSet);
return simpleAuthorizationInfo;
}
/**
* 进行认证
*
* @param authenticationToken 数据源
* @return 认证结果
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
// 根据 Token 获取数据
String username = (String) authenticationToken.getPrincipal();
// 根据用户名从数据库中查询该用户
UserInfo user = getUserInfo(username);
if (user != null) {
// 把当前用户存到 Session 中
SecurityUtils.getSubject().getSession().setAttribute("user", user);
// 传入用户名和密码进行身份认证,并返回认证信息
AuthenticationInfo authInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), this.getName());
return authInfo;
} else {
//未查找到用户信息,认证失败
return null;
}
}
}
5.4.配置shiro
package com.yezi_tool.basic_project.config;
import com.yezi_tool.basic_project.shiro.MyRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author Echo_Ye
* @title shiro配置
* @description shiro相关配置
* @date 2020/8/17 11:28
* @email echo_yezi@qq.com
*/
@Slf4j
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
// 定义bean
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 认证失败url
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 开放静态资源权限
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/assets/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
// 开放swagger页面权限
filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
// 开放登陆接口
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "logout");
// 开放公共接口
filterChainDefinitionMap.put("/mongodbFile/getImage", "anon");
filterChainDefinitionMap.put("/sms/**", "anon");
// 其他页面权限限制
// 其余全部接口必须进行身份验证,此处方便测试限制到shiroTest开头
filterChainDefinitionMap.put("/shiroTest/**", "authc");
// "/teacher/"相关接口必须有teacher权限
filterChainDefinitionMap.put("/shiroTest/teacher/**", "perms[teacher]");
// "/student/"相关接口必须有student权限
filterChainDefinitionMap.put("/shiroTest/student/**", "perms[student]");
//设置拦截器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("-----------------shiro配置注入完成---------------------");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(customRealm());
return securityManager;
}
/**
* 自定义身份认证 realm
*/
@Bean
public MyRealm customRealm() {
return new MyRealm();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
拦截器常用权限参数列表
- anon:开放权限,可直接访问
- authc:需要认证
- logout:注销,执行后跳转至登录页面,即
shiroFilterFactoryBean.setLoginUrl()
所设置的页面 - roles[role1, role2...]:所需角色。若有多个角色,则需满足所有角色才可放行。
- perms[permission1, permission2...]:所需权限。若有多个权限,则需满足所有权限才可放行。
6.使用样例(仅供参考思路,正常使用请参考贴在后面的整理后的核心代码)
6.1.登录接口(正常使用请勿返回账号数据)
package com.yezi_tool.basic_project.controller;
import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import com.yezi_tool.basic_project.entity.UserInfo;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author Echo_Ye
* @title 登录接口
* @description 用于登录相关接口
* @date 2020/8/17 9:39
* @email echo_yezi@qq.com
*/
@Controller
@RequestMapping("/login")
public class LoginController {
@Data
public static class LoginRequest {
private String username;
private String password;
}
@PostMapping("/login")
@ResponseBody
public ReturnMsg login(@RequestBody LoginRequest loginRequest) {
ReturnMsg returnMsg = new ReturnMsg();
//组装token
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginRequest.username, loginRequest.password);
try {
//执行登录
subject.login(token);
//获取验证后的对象
UserInfo userInfo = (UserInfo) subject.getPrincipal();
//检查权限,无权限将会抛出异常
// subject.checkPermission("teacher");
returnMsg.setData(userInfo);
} catch (UnknownAccountException e) {
returnMsg = ReturnMsg.error("账号不存在");
} catch (IncorrectCredentialsException e) {
returnMsg = ReturnMsg.error("密码错误");
} catch (LockedAccountException e) {
returnMsg = ReturnMsg.error("账号被锁定");
} catch (AuthenticationException e) {
returnMsg = ReturnMsg.error("认证错误");
} catch (UnauthorizedException e) {
returnMsg = ReturnMsg.error("权限错误");
}
return returnMsg;
}
}
6.2.测试权限接口
package com.yezi_tool.basic_project.controller;
import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
/**
* @author Echo_Ye
* @title shiro测试接口
* @description 用于shiro相关测试
* @date 2020/8/17 11:54
* @email echo_yezi@qq.com
*/
@Controller
@RequestMapping("/shiroTest")
public class ShiroTestController {
@GetMapping("teacher/testShiro")
@ResponseBody
public ReturnMsg teacherTest(HttpServletRequest request) {
return ReturnMsg.success();
}
@GetMapping("student/testShiro")
@ResponseBody
public ReturnMsg studentTest(HttpServletRequest request) {
return ReturnMsg.success();
}
@GetMapping("checkPermission")
@ResponseBody
public ReturnMsg checkPermission(HttpServletRequest request, String permission) {
SecurityUtils.getSubject().checkPermission(permission);
return ReturnMsg.success();
}
@GetMapping("checkSuperAdmin")
@ResponseBody
@RequiresPermissions({"teacher","student"})
public ReturnMsg checkSuperAdmin(HttpServletRequest request) {
return ReturnMsg.success();
}
}
6.3.进行测试
使用postman,每次使用同一个token进行测试,操作和结果如下
-
发起请求
/shiroTest/student/testShiro
,结果为404的ModelAndView
对象,提示/login.html
不存在未进行登录,故跳转至登录页面,而测试项目并没有这个页面
-
发起请求
LoginController/login
,结果为用户信息,即登录成功 -
再次发起请求
/shiroTest/student/testShiro
,结果为请求成功,则证明权限验证有效 -
发起请求
/shiroTest/checkSuperAdmin
,结果为操作失败,后端报错Subject does not have permission [teacher]
该接口使用了注解,需要
student
和teacher
两个权限,缺少任意一个则会抛出异常
7.补充
7.1.SimpleAuthorizationInfo权限信息常用操作
addRole(String role)
,addRoles(Collection<String> roles)
,setRoles(Set<String> roles)
:添加/批量添加/批量设置角色addStringPermission(String permission)
,addStringPermissions(Collection<String> permissions)
,setStringPermissions(Set<String> roles)
:添加/批量添加/批量设置权限
7.2.认证主体Subject常用操作
login(AuthenticationToken var1)
:登录,失败会抛出异常logout()
:登出,注销认证信息getPrincipal()
:获取认证身份信息,通常读取账号或者全部账号数据getSession().setAttribute(Object var1, Object var2)
,getSession().getAttribute(Object var1)
:设置/获取session缓存数据getSession().setTimeout(long var1)
,getSession().getTimeout()
:设置/获取session有限时限isPermitted(String var1)
:是否满足权限,参数也可为Permission var1
,String... var1
,List<Permission> var1
isPermittedAll(String... var1)
:是否满足全部权限,参数也可为Collection<Permission> var1
checkPermission(String var1)
:检查权限,权限不足时抛出异常,参数也可为Permission var1
,String... var1
、Collection<Permission> var1
hasRole(String var1)
:是否满足角色,参数也可为List<String> var1
hasAllRoles(Collection<String> var1)
:是否满足全部角色checkRole(String var1)
:检查角色,角色不足时抛出异常checkRoles(String... var1)
:检查角色,角色不足时抛出异常,参数也可为Collection<String> var1
isRemembered()
:是否记住登录状态isAuthenticated()
:是否已认证,没错,认证主体并不一定已被认证
7.3.拦截器常用权限参数列表(再重提一遍,很重要)
Filter | 解释 |
---|---|
anon | 无参,开放权限,可以理解为匿名用户或游客 |
authc | 无参,需要认证 |
logout | 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl(); 设置的 url |
authcBasic | 无参,表示 httpBasic 认证 |
user | 无参,表示必须存在用户,当登入操作时不做检查 |
ssl | 无参,表示安全的URL请求,协议为 https |
perms[user] | 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过 |
roles[admin] | 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过 |
rest[user] | 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等 |
port[8081] | 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数 |
此外,权限参数可以自定义,详情请自行查阅资料,或者等我啥时候有空了整理。。。
7.4.控制层注解
-
@RequiresAuthentication
验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。
-
@RequiresUser
验证用户是否被记忆,user有两种含义:
一种是成功登录的(subject.isAuthenticated() 结果为true);
另外一种是被记忆的(subject.isRemembered()结果为true)。
-
@RequiresGuest
验证是否是一个guest的请求,与@RequiresUser完全相反。
换言之,RequiresUser == !RequiresGuest。
此时subject.getPrincipal() 结果为null.
-
@RequiresRoles
例如:
- @RequiresRoles("roleA")
- @RequiresRoles({"roleA", “roleB"})
验证角色,任意一个不符合则抛出异常
-
@RequiresPermissions
例如:
-
@RequiresPermissions("file")
-
@RequiresPermissions({"file:read", "file:write"} )
验证权限,任意一个不符合则抛出异常
-
8.整理后代码
8.1.整理内容
- 优化代码结构
- 查询账号方法修正为从数据库查询(使用mybatisPlus)
- 密码改为密文(MD5加密+盐)
- 登录结果仅提示成功或失败
- shiro相关异常使用全局异常捕获
- 自定义token对象,并添加其余参数(如验证码,登录端),并进行验证
- 通用方法写到BaseController
- 增加多地登录踢下线机制
- 缓存改用redis
- 增加RememberMe机制
相关内容会另起他文整理,此处不做赘述,有兴趣的不妨瞅一下我更新没。。。
8.2.核心源码:
ShiroConfig.java
package com.yezi_tool.basic_project.config;
import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.shiro.KickoutSessionControlFilter;
import com.yezi_tool.basic_project.shiro.MyRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.context.annotation.DependsOn;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
/**
* @author Echo_Ye
* @title shiro配置
* @description shiro相关配置
* @date 2020/8/17 11:28
* @email echo_yezi@qq.com
*/
@Slf4j
@Configuration
public class ShiroConfig { //获取application.properties参数,此处不能加static关键字
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String redisPassword;
/**
* Shiro生命周期处理器
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
log.info("-----------------Shiro生命周期周期处理器设置---------------------");
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
@ConditionalOnMissingBean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
// 定义bean
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 认证失败url
shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
// 登录成功url
// shiroFilterFactoryBean.setSuccessUrl("/index");
//当访问受限url
// shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
//自定义拦截器
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
//限制同一帐号同时在线的个数。
filtersMap.put("kickout", kickoutSessionControlFilter());
shiroFilterFactoryBean.setFilters(filtersMap);
// 设置拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 开放静态资源权限
filterChainDefinitionMap.put("/webjars/**", "anon");
filterChainDefinitionMap.put("/assets/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
// 开放swagger页面权限
filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
filterChainDefinitionMap.put("/swagger-ui.html", "anon");
// 开放登陆接口
filterChainDefinitionMap.put("/index", "anon");
filterChainDefinitionMap.put("/login/**", "anon");
filterChainDefinitionMap.put("/logout", "logout");
// 开放公共接口
filterChainDefinitionMap.put("/mongodbFile/getImage", "anon");
filterChainDefinitionMap.put("/sms/**", "anon");
// 其他页面权限限制
// 其余全部接口必须进行身份验证,且强制下线
filterChainDefinitionMap.put("/**", "user,kickout");
// "/teacher/"相关接口必须有teacher权限
filterChainDefinitionMap.put("/shiroTest/teacher/**", "perms[teacher]");
// "/student/"相关接口必须有student权限
filterChainDefinitionMap.put("/shiroTest/student/**", "perms[student]");
//设置拦截器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
log.info("-----------------shiro配置注入完成---------------------");
return shiroFilterFactoryBean;
}
/**
* 注入 securityManager
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置realm
securityManager.setRealm(customRealm());
// 自定义缓存实现 使用redis
securityManager.setCacheManager(redisCacheManager());
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager());
// 自定义rememberMe管理
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
/**
* 自定义身份认证 realm
*/
@Bean
public MyRealm customRealm() {
MyRealm myRealm = new MyRealm();
//设置加密方式
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
/**
* 加密策略
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//指定加密方式
credentialsMatcher.setHashAlgorithmName(ConfigConstants.SHIRO_ENCODE_MODE);
//加密次数
credentialsMatcher.setHashIterations(ConfigConstants.SHIRO_ENCODE_TIMES);
//此处的设置,true加密用的hex编码,false用的base64编码
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* cacheManager 缓存 redis实现
* 使用的是shiro-redis开源插件
*/
public RedisCacheManager redisCacheManager() {
log.info("-----------------创建缓存管理器---------------------");
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
//redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
redisCacheManager.setPrincipalIdFieldName("id");
// 用户权限信息缓存时间
// redisCacheManager.setExpire(200000);
return redisCacheManager;
}
/**
* Session Manager
* 使用的是shiro-redis开源插件
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* RedisSessionDAO shiro sessionDao层的实现 通过redis
* 使用的是shiro-redis开源插件
*/
@Bean
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return redisSessionDAO;
}
/**
* 配置shiro redisManager
* 使用的是shiro-redis开源插件
*/
public RedisManager redisManager() {
log.info("-----------------创建RedisManager,连接Redis..URL= " + host + ":" + port + "---------------------");
RedisManager redisManager = new RedisManager();
redisManager.setHost(host + ":" + port);//老版本是分别setHost和setPort,新版本只需要setHost就可以了
if (!StringUtils.isEmpty(redisPassword)) {
redisManager.setPassword(redisPassword);
}
return redisManager;
}
/**
* 限制同一账号登录同时登录人数控制
*/
@Bean
public KickoutSessionControlFilter kickoutSessionControlFilter() {
KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
kickoutSessionControlFilter.setCacheManager(redisCacheManager());
kickoutSessionControlFilter.setSessionManager(sessionManager());
kickoutSessionControlFilter.setKickoutAfter(false);
kickoutSessionControlFilter.setMaxSession(ConfigConstants.SHIRO_SESSION_KICKOUT_MAX_SESSION);
kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");
return kickoutSessionControlFilter;
}
/**
* Cookie
*/
@Bean
public SimpleCookie rememberMeCookie() {
//这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie(ConfigConstants.SHIRO_COOKIE_KEY_REMEMBER_ME);
//如果httpOnly设置为true,则客户端不会暴露给客户端脚本代码,使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击;
simpleCookie.setHttpOnly(true);
//记住我cookie生效时间,单位是秒
simpleCookie.setMaxAge(600);
return simpleCookie;
}
/**
* cookie管理器;
*/
@Bean
public CookieRememberMeManager rememberMeManager() {
log.info("-----------------创建RememberMe管理器---------------------");
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
//rememberme cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
byte[] cipherKey = Base64.decode(ConfigConstants.SHIRO_COOKIE_CIPHER_KEY);
cookieRememberMeManager.setCipherKey(cipherKey);
cookieRememberMeManager.setCookie(rememberMeCookie());
return cookieRememberMeManager;
}
}
MyRealm.java
package com.yezi_tool.basic_project.shiro;
import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.entity.UserInfo;
import com.yezi_tool.basic_project.service.IUserInfoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Echo_Ye
* @title 自定义realm
* @description 自定义数据域
* @date 2020/8/17 9:25
* @email echo_yezi@qq.com
*/
@Slf4j
public class MyRealm extends AuthorizingRealm {
/**
* 用户信息业务层
*/
@Autowired
@Qualifier("userInfoService")
private IUserInfoService userInfoService;
/**
* 分配权限
*
* @param principalCollection 数据源
* @return 权限和角色信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("-----------------访问授权---------------------");
//认证信息未登录,可能是用户非正常退出
if (!SecurityUtils.getSubject().isAuthenticated()) {
doClearCache(principalCollection);
SecurityUtils.getSubject().logout();
return null;
}
//获取认证主体
UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
if (userInfo == null) {
//认证错误
return null;
}
String username = userInfo.getUsername();
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<String> permissionInfoList = userInfoService.queryPermission(username);
Set<String> permissionSet = permissionInfoList.stream().collect(Collectors.toSet());
simpleAuthorizationInfo.setStringPermissions(permissionSet);
// 存储信息到session,根据自身需求添加
SecurityUtils.getSubject().getSession().setAttribute(ConfigConstants.SHIRO_SESSION_KEY_USER_INFO, userInfo);
return simpleAuthorizationInfo;
}
/**
* 进行认证
*
* @param authenticationToken 数据源
* @return 认证结果
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
//获取自定义token
CustomAuthenticationToken token = (CustomAuthenticationToken) authenticationToken;
// 根据 Token 获取数据
String username = (String) token.getPrincipal();
// 根据用户名从数据库中查询该用户
UserInfo user = userInfoService.selectByUserName(username);
if (user != null) {
// 传入用户名和密码进行身份认证,并返回认证信息
//自定义盐值
ByteSource salt = ByteSource.Util.bytes(user.getSalt());
AuthenticationInfo authInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, this.getName());
return authInfo;
} else {
//未查找到用户信息,认证失败
return null;
}
}
}
ConfigConstants.java
package com.yezi_tool.basic_project.commons.constants;
/**
* @author Echo_Ye
* @title 配置信息常量
* @description 用于各种配置
* @date 2020/8/19 18:13
* @email echo_yezi@qq.com
*/
public class ConfigConstants {
/**
* 序列化相关
*/
public static final long SERIAL_VERSION_UID = 1L; //序列号
/**
* shiro相关
*/
public static final String SHIRO_ENCODE_MODE = "MD5"; //加密方式
public static final int SHIRO_ENCODE_TIMES = 1; //加密次数
public static final String SHIRO_REDIS_PREFIX_CACHE = "shiro_redis_cache"; //缓存前缀
public static final String SHIRO_SESSION_PREFIX_KICKOUT = "kickout"; //缓存前缀
public static final int SHIRO_SESSION_KICKOUT_MAX_SESSION = 1; //踢下线最多人数
public static final String SHIRO_SESSION_KEY_USER_INFO = "user_info"; //缓存-用户信息
public static final String SHIRO_COOKIE_KEY_REMEMBER_ME = "rememberMe"; //cookie-记住我
public static final String SHIRO_COOKIE_CIPHER_KEY = "qU7b1ChYNwqbrEwTlPbO9Q=="; //cookie-加密秘钥
/**
* redis相关
*/
public static final String REDIS_KEY_CAPTCHA="captcha";
}
BaseExceptionHandler.java
package com.yezi_tool.basic_project.interceptors;
import com.yezi_tool.basic_project.commons.constants.ResponseConstants;
import com.yezi_tool.basic_project.commons.exception.BaseException;
import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import com.yezi_tool.basic_project.commons.utils.ObjectMapperFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.NestedServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @title 统一异常处理
* @description 统一处理项目内的异常
* @author Echo_Ye
* @date 2020/8/24 10:42
* @email echo_yezi@qq.com
*/
@Slf4j
@ControllerAdvice
public class BaseExceptionHandler {
/**
* 统一异常处理,仅处理NestedServletException
*/
@ExceptionHandler({NestedServletException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ModelAndView servletException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
ModelAndView mav = new ModelAndView();
ReturnMsg message = ReturnMsg.FAIL;
out(response, message);
return mav;
}
/**
* 统一异常处理,仅处理ShiroException
*/
@ExceptionHandler({ShiroException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ReturnMsg shiroException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
// 打印异常信息至控制台,开始处理异常
log.error("异常统一处理-ShiroException:" + exception.getLocalizedMessage(), exception);
//异常默认为是操作失败
ReturnMsg message = ReturnMsg.FAIL;
//确认错误类型
if (exception instanceof UnknownAccountException) {
//账号错误
message.setMsg(ResponseConstants.SHIRO_MSG_UNKNOWN_ACCOUNT);
} else if (exception instanceof IncorrectCredentialsException) {
//密码错误
message.setMsg(ResponseConstants.SHIRO_MSG_INCORRECT_CREDENTIALS);
} else if (exception instanceof LockedAccountException) {
//账号被锁定
message.setMsg(ResponseConstants.SHIRO_MSG_LOCKED_ACCOUNT);
} else if (exception instanceof AuthenticationException) {
//认证错误
message.setMsg(ResponseConstants.SHIRO_MSG_AUTHENTICATION_ERROR);
} else if (exception instanceof UnauthorizedException) {
//权限不足
message.setMsg(ResponseConstants.SHIRO_MSG_UNAUTHORIZED);
}
//返回消息体
return message;
}
/**
* 统一异常处理
*/
@ExceptionHandler({Exception.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public ReturnMsg processException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
// 打印异常信息至控制台,开始处理异常
log.error("异常统一处理-Exception:" + exception.getLocalizedMessage(), exception);
//异常默认为是操作失败
ReturnMsg message = ReturnMsg.FAIL;
// 检查异常的类型
if (exception instanceof NestedServletException) {
// 异步请求错误,已处理
} else if (exception instanceof BaseException) {
// 自定义类型的异常,转换为自定义异常
message = ((BaseException) exception).asReturnMsg();
} else {
// 非自定义类型异常,打印错误信息至日志,封装ReturnMsg对象
log.error(request.getRequestURI(), exception);
message = ReturnMsg.FAIL;
}
//返回消息体
return message;
}
/**
* response 输出JSON
*/
public static void out(ServletResponse response, ReturnMsg returnMsg) {
PrintWriter out = null;
try {
response.setContentType("application/json;charset=utf-8");
out = response.getWriter();
out.println(ObjectMapperFactory.getInstance().writeValueAsString(returnMsg));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != out) {
out.flush();
out.close();
}
}
}
}
CustomAuthenticationToken.java
package com.yezi_tool.basic_project.shiro;
import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import lombok.Data;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
/**
* @author Echo_Ye
* @title 自定义token
* @description 自定义token,适用于shiro
* @date 2020/8/20 11:56
* @email echo_yezi@qq.com
*/
@Data
public class CustomAuthenticationToken extends UsernamePasswordToken {
/**
* 序列号
*/
private static final long serialVersionUID = ConfigConstants.SERIAL_VERSION_UID;
/**
* 验证码字段
*/
public static final String DEFAULT_CAPTCHA_PARAM = "captcha";
/**
* 登录类型
*/
private int loginType;
/**
* 登录方式
*/
private String loginMode;
/**
* 手机验证码
*/
private String code;
/**
* 验证码
*/
private String captcha;
/**
* 从request请求中获取验证码
*/
public String getCaptcha(ServletRequest request) {
return WebUtils.getCleanParam(request, DEFAULT_CAPTCHA_PARAM);
}
public CustomAuthenticationToken(String username, String password) {
super(username, password);
}
public CustomAuthenticationToken(String username, String password, String captcha) {
super(username, password);
this.captcha = captcha;
}
}
KickoutSessionControlFilter.java
package com.yezi_tool.basic_project.shiro;
import com.alibaba.fastjson.JSON;
import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.entity.UserInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
public class KickoutSessionControlFilter extends AccessControlFilter {
private String kickoutUrl; //踢出后到的地址
private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
private int maxSession = 1; //同一个帐号最大会话数 默认1
private String kickoutKey = ConfigConstants.SHIRO_SESSION_PREFIX_KICKOUT;//提出字段名
private SessionManager sessionManager;
private Cache<String, Deque<Serializable>> cache;
public void setKickoutUrl(String kickoutUrl) {
this.kickoutUrl = kickoutUrl;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
//设置Cache的key的前缀
public void setCacheManager(CacheManager cacheManager) {
this.cache = cacheManager.getCache(ConfigConstants.SHIRO_REDIS_PREFIX_CACHE);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
if (!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,直接进行之后的流程
return true;
}
Session session = subject.getSession();
UserInfo user = (UserInfo) subject.getPrincipal();
String username = user.getUsername();
Serializable sessionId = session.getId();
//读取缓存 没有就存入
Deque<Serializable> deque = cache.get(username);
//如果此用户没有session队列,也就是还没有登录过,缓存中没有
//就new一个空队列,不然deque对象为空,会报空指针
if (deque == null) {
deque = new LinkedList<Serializable>();
}
//如果队列里没有此sessionId,且用户没有被踢出;放入队列
if (!deque.contains(sessionId) && session.getAttribute(kickoutKey) == null) {
//将sessionId存入队列
deque.push(sessionId);
//将用户的sessionId队列缓存
cache.put(username, deque);
}
//如果队列里的sessionId数超出最大会话数,开始踢人
while (deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if (kickoutAfter) { //如果踢出后者
kickoutSessionId = deque.removeFirst();
//踢出后再更新下缓存队列
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
//踢出后再更新下缓存队列
}
cache.put(username, deque);
try {
//获取被踢出的sessionId的session对象
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if (kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute(kickoutKey, true);
}
} catch (Exception e) {//ignore exception
}
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute(kickoutKey) != null) {
//会话被踢出了
try {
//退出登录
subject.logout();
} catch (Exception e) { //ignore
}
saveRequest(request);
Map<String, String> resultMap = new HashMap<String, String>();
//判断是不是Ajax请求
if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
resultMap.put("code ", "300");
resultMap.put("message", "您已经在其他地方登录,请重新登录!");
//输出json串
out(response, resultMap);
} else {
//重定向
WebUtils.issueRedirect(request, response, kickoutUrl);
}
return false;
}
return true;
}
private void out(ServletResponse hresponse, Map<String, String> resultMap) throws IOException {
try {
hresponse.setCharacterEncoding("UTF-8");
PrintWriter out = hresponse.getWriter();
out.println(JSON.toJSONString(resultMap));
out.flush();
out.close();
} catch (Exception e) {
System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
}
}
}
BaseController.java
package com.yezi_tool.basic_project.controller;
import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.commons.constants.ResponseConstants;
import com.yezi_tool.basic_project.commons.exception.BaseException;
import com.yezi_tool.basic_project.shiro.CustomAuthenticationToken;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Controller;
@Controller
public class BaseController {
/**
* 校验验证码
*
* @param token token对象
* @throws Exception 校验失败则抛出异常
*/
public void checkCaptcha(CustomAuthenticationToken token) throws Exception {
String captcha = token.getCaptcha();
String exitCode = (String) SecurityUtils.getSubject().getSession().getAttribute(ConfigConstants.REDIS_KEY_CAPTCHA);
if (null == captcha || !captcha.equalsIgnoreCase(exitCode)) {
throw new BaseException(ResponseConstants.RETURN_MSG_INCORRECT_CAPTCHA);
}
}
/**
* 获取认证者身份
*
* @return 认证者身份信息
*/
public Object getPrimaryPrincipal() {
return SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
}
/**
* 获取session字段值
*
* @param key session的字段key
* @return session的字段值
*/
public Object getSessionAttribute(String key) {
return SecurityUtils.getSubject().getSession().getAttribute(key);
}
}
LoginController.java
package com.yezi_tool.basic_project.controller;
import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import com.yezi_tool.basic_project.shiro.CustomAuthenticationToken;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
/**
* @author Echo_Ye
* @title 登录接口
* @description 用于登录相关接口
* @date 2020/8/17 9:39
* @email echo_yezi@qq.com
*/
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {
@Data
private static class LoginRequest {
private String username;
private String password;
private Boolean rememberMe;
}
@PostMapping("/login")
@ResponseBody
public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
//组装token
Subject subject = SecurityUtils.getSubject();
CustomAuthenticationToken token = new CustomAuthenticationToken(loginRequest.username, loginRequest.password);
// 设置rememberMe字段
if(loginRequest.rememberMe!=null) {
token.setRememberMe(loginRequest.rememberMe);
}
//判断验证码,暂不启用
// checkCaptcha(token);
//执行登录
subject.login(token);
return ReturnMsg.success();
}
}
8.3.全部代码:
demo地址:https://gitee.com/echo_ye/shiro-demo
demo已能正常运转预期所有功能,但仅供参考,请视实际业务自行删减和修改,有疑问或者建议可以留言或者联系我~
BB两句
真的是了解越多,越觉得其强大,越觉得自己弱鸡。。。
作者:Echo_Ye
WX:Echo_YeZ
EMAIL :echo_yezi@qq.com
个人站点:在搭了在搭了。。。(右键 - 新建文件夹)