前言
-
Shiro是Apache下的一个开源项目,用于身份和权限验证的轻量级框架,较Spring Security配置和使用简单,且这次项目对权限控制的细粒度不高。
-
项目环境
SpringBoot 2.1.5 + Redis + Mybatis-plus
因为需要缓存用户信息,所以前期需要先搭建redis(可参考redis集群搭建)
然后下图是项目所需配置的所有文件:
- 因为我们的新ERP是基于原本老ERP(PHP语言)改造的,需要兼容老系统用户信息且新增了app端的部分,所以有以下几点需要考虑:
- 登录方式要多种,账号密码和账号验证码等
- 要区分不同的客户端,app端登录失效时间比web端要长
- 兼容数据中老ERP的用户账号密码信息,密码加密方式需要和原本php的加密方式保持一致
话不多说,直接开干。
引入依赖
<!--
<shiro-spring>1.4.1</shiro-spring>
<shiro-redis>3.1.0</shiro-redis>
-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro-spring}</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>${shiro-redis}</version>
</dependency>
项目搭建
Realm
-
功能介绍
Realm能做的工作主要有以下几个方面:-
身份验证(
getAuthenticationInfo
方法)验证账户和密码,并返回相关信息 -
权限获取(
getAuthorizationInfo
方法) 获取指定身份的权限,并返回相关信息 -
令牌支持(
supports
方法)判断该令牌(Token)是否被支持
令牌有很多种类型,例如:HostAuthenticationToken(主机验证令牌),UsernamePasswordToken(账户密码验证令牌),也可自己定义,只需要继承
AuthorizingRealm
本次既要支持账号密码,也要支持手机号验证码,故定义了两个Realm验证,验证策略为:AtLeastOneSuccessfulStrategy
,即至少一个认证通过就算通过
密码的匹配方式也可根据自身需要选择或定义
-
- 自定义账号密码Realm:PasswordRealm
@Component
@DependsOn("lifecycleBeanPostProcessor")
public class PasswordRealm extends AuthorizingRealm {
// @Resource
// private IUserService userService;
@Resource
private UserMapper userMapper;
@Resource
private UserPermissionsMapper userPermissionsMapper;
/**
* 特殊的身份认证方法,判断当前用户是否为超管用户
*
* @param principals
* @param permission
* @return boolean
*/
@Override
public boolean isPermitted(PrincipalCollection principals, String permission) {
/*
* 获得当前用户信息
* 验证用户类型 当类型为GOD时返回true
* 当类型为OTHER时 再进行权限匹配
* 当用户中权限拥有当前访问类或方法的权限时 返回true
*
* */
User primaryPrincipal = (User) principals.getPrimaryPrincipal();
if (primaryPrincipal.getType().equals(UserTypeEnum.GOD.getType())) {
return true;
} else {
return super.isPermitted(principals, permission);
}
}
/**
* 权限认证方法
*
* @param principals
* @return AuthorizationInfo
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserVO user = (UserVO) principals.getPrimaryPrincipal();
for (String permission : user.getPermissions()) {
authorizationInfo.addStringPermission(permission);
}
return authorizationInfo;
}
/**
* 登录认证
*
* @param authenticationToken
* @return AuthenticationInfo
* @throws AuthenticationException
* @throws LockedAccountException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException, LockedAccountException {
CustomUsernamePasswordToken token = (CustomUsernamePasswordToken) authenticationToken;
String phone = token.getUsername();
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getPhone, token.getUsername()));
UserVO userVO = BeanUtil.toBean(user, UserVO.class);
List<UserPermissions> list = userPermissionsMapper.selectList(new QueryWrapper<UserPermissions>().lambda().eq(UserPermissions::getUid, userVO.getUid()));
userVO.setPermissions(list.stream().map(UserPermissions::getPermission).collect(Collectors.toSet()));
//如果不想自定义密码验证的方法,也可在此验证密码是否正确
// if (!StringUtil.md5(String.valueOf(token.getCredentials()) + user.getSalt()).equals(user.getPassword())){
// throw new IncorrectCredentialsException();
// }
// SecurityUtils.getSubject().getSession().setAttribute("loginType", token.getClientType());
// return new SimpleAuthenticationInfo(user,user.getPassword(),getName());
//登录设备类型放入session中
SecurityUtils.getSubject().getSession().setAttribute("loginType", token.getClientType());
//构造authenticationInfo
return new SimpleAuthenticationInfo(
userVO,
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()),
getName()
);
}
/**
* 自定义密码匹配方式
*
* @return CredentialsMatcher
*/
@Override
public CredentialsMatcher getCredentialsMatcher() {
return (authenticationToken, authenticationInfo) -> {
// 用户填写的登录密码
char[] passwordChars = (char[]) authenticationToken.getCredentials();
String password = new String(passwordChars);
/**
* 这里这么做的原因是shiro会自动把token中的密码给转成字符数组
*
* public UsernamePasswordToken(final String username, final String password) {
* this(username, password != null ? password.toCharArray() : null, false, null);
* }
*/
UserVO user = (UserVO) authenticationInfo.getPrincipals().getPrimaryPrincipal();
// 加密密码
String credentials = (String) authenticationInfo.getCredentials();
return StringUtil.md5(password + user.getSalt()).equals(credentials);
};
}
/**
* 判断token是否支持当前的 realm
*
* @param token 传入的token
* @return true就使用,false就不使用
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof CustomUsernamePasswordToken;
}
}
- 自定义手机验证码Realm:PhoneSmsCodeRealm
@Component
@DependsOn("lifecycleBeanPostProcessor")
public class PhoneSmsCodeRealm extends AuthorizingRealm {
// @Resource
// private IUserService userService;
@Resource
private UserMapper userMapper;
@Resource
private UserPermissionsMapper userPermissionsMapper;
/**
* 权限验证
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserVO userVO = (UserVO) principals.getPrimaryPrincipal();
for (String permission : userVO.getPermissions()) {
authorizationInfo.addStringPermission(permission);
}
return authorizationInfo;
}
/**
* 登录验证
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
PhoneSmsCodeToken token = (PhoneSmsCodeToken) authenticationToken;
String phone = token.getPhone();
//登录设备类型放入session中
SecurityUtils.getSubject().getSession().setAttribute("loginType", token.getClientType());
// UserVO user = userService.getUserVOByPhone(phone);
User user = userMapper.selectOne(new QueryWrapper<User>().lambda().eq(User::getPhone, token.getUsername()));
UserVO userVO = BeanUtil.toBean(user, UserVO.class);
List<UserPermissions> list = userPermissionsMapper.selectList(new QueryWrapper<UserPermissions>().lambda().eq(UserPermissions::getUid, userVO.getUid()));
userVO.setPermissions(list.stream().map(UserPermissions::getPermission).collect(Collectors.toSet()));
//如果AuthenticationInfo和AuthenticationToken中的
// credentials不一致且未设置正确的CredentialsMatcher的话
// 会抛出密码错误异常: IncorrectCredentialsException
return new SimpleAuthenticationInfo(userVO, user.getPassword(), getName());
}
@Override
public CredentialsMatcher getCredentialsMatcher() {
return new AllowAllCredentialsMatcher();
}
/**
* 判断token是否支持当前的 realm
*
* @param token 传入的token
* @return true就使用,false就不使用
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof PhoneSmsCodeToken;
}
PS:
这里有一个超级无敌的大坑!!我花了好多时间才发现,而且还是JRebel这个热部署插件给我提供的灵感!!在此感谢~
如果你的realm中引入了一些XXXService,而且恰好本身又是一个多数据源,需要实现分布式事务(仅指Atomikos)的项目
那么恭喜你,这些Service中的所有接口的事务控制都会失效!
多个数据源之间切换时,假如某个服务抛异常,那么在此之前已经执行的sql,都会直接commit,无法回滚.
详情请戳主页查看,此处不做赘述.
AuthenticationToken
AuthenticationToken用于保存用户提交的身份(账号/手机号/邮箱等)及凭据(密码)
理论上其实一个自定义的token即可,只需要实现HostAuthenticationToken
和````RememberMeAuthenticationToken即可 但为了区分两种认证方式,故分为账号密码:
UsernamePasswordToken和账号验证码:
PhoneSmsCodeToken```
- UsernamePasswordToken
/**
* 自定义用户账号密码token,继承了原本自带的UsernamePasswordToken,只新增了个字段:登录设备类型
*
* @author : itoyoung
* @date : 2021-03-04 14:45
*/
public class CustomUsernamePasswordToken extends UsernamePasswordToken {
//登录设备类型:app 、web
private String clientType;
public String getClientType() {
return clientType;
}
public void setClientType(String clientType) {
this.clientType = clientType;
}
public CustomUsernamePasswordToken(String username, String password, boolean rememberMe, String clientType) {
super(username, password, rememberMe);
this.clientType = clientType;
}
}
- PhoneSmsCodeToken
/**
* 自定义用户账号验证码token
*
* @author : itoyoung
* @date : 2021-03-04 14:45
*/
public class PhoneSmsCodeToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
private String phone;
private boolean rememberMe;
private String host;
//登录设备类型:app 、web
private String clientType;
public PhoneSmsCodeToken() {
}
public PhoneSmsCodeToken(String phone, String clientType, boolean rememberMe) {
this.phone = phone;
this.rememberMe = rememberMe;
this.clientType = clientType;
}
public PhoneSmsCodeToken(String phone, String host, String clientType, boolean rememberMe) {
this(phone, clientType, rememberMe);
this.host = host;
}
@Override
public String getHost() {
return host;
}
@Override
public boolean isRememberMe() {
return rememberMe;
}
@Override
public Object getPrincipal() {
return phone;
}
@Override
public Object getCredentials() {
return phone;
}
public String getPhone() {
return phone;
}
public String getClientType() {
return clientType;
}
}
- 配置shiro session缓存到redis中,实现从redis存储和读取用户信息
/**
* redisSessionDAO shiro sessionDAO层的实现 通过redis
* 使用的是shiro-redis开源插件
*
* @author oyj
* @date 2019-06-23 22:36
*/
@Component
public class RedisSessionDAO extends AbstractSessionDAO {
@Resource(name = "shiroRedisTemplate")
private RedisTemplate redisTemplate;
// Session失效时间,单位为毫秒
private long expireTime = 1000 * 60 * 30;
private String prefix = "SHIRO:SESSIONID:";
public RedisSessionDAO() {
super();
}
public RedisSessionDAO(long expireTime, RedisTemplate redisTemplate) {
super();
this.expireTime = expireTime;
this.redisTemplate = redisTemplate;
}
// 更新session
@Override
public void update(Session session) throws UnknownSessionException {
if (session == null || session.getId() == null) {
return;
}
//app登录永不过期
long timeout = expireTime;
if ("app".equals(session.getAttribute("loginType"))) {
timeout = -1000;
redisTemplate.opsForValue().set(prefix + session.getId(), session);
} else {
redisTemplate.opsForValue().set(prefix + session.getId(), session, timeout, TimeUnit.MILLISECONDS);
}
session.setTimeout(timeout);
}
// 删除session
@Override
public void delete(Session session) {
if (null == session) {
return;
}
redisTemplate.opsForValue().getOperations().delete(prefix + session.getId());
}
// 获取活跃的session,可以用来统计在线人数,如果要实现这个功能
// 可以在将session加入redis时指定一个session前缀
// 统计的时候则使用keys("prefix + *")的方式来模糊查找redis中所有的session集合
@Override
public Collection<Session> getActiveSessions() {
return redisTemplate.keys(prefix + "*");
}
// 加入session
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
return sessionId;
}
// 读取session
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
Session session = (Session) redisTemplate.opsForValue().get(prefix + sessionId);
return session;
}
// 设置redis序列化方式,既可在RedisConf中设置,也可在set方法中设置,但是注意:
// 1. 一定要设置为StringRedisSerializer,否则session的key会变成乱码
// 2. 只能设置setKeySerializer()和setHashKeySerializer()两种序列化方式,否则之后读取session会报序列化异常
// @Resource
// public void setRedisTemplate(RedisTemplate redisTemplate) {
// RedisSerializer<String> stringSerializer = new StringRedisSerializer();
// redisTemplate.setKeySerializer(stringSerializer);
// redisTemplate.setHashKeySerializer(stringSerializer);
// this.redisTemplate = redisTemplate;
// }
}
//shiro redis配置文件
@Configuration
public class RedisConfig {
@Bean(value = "shiroRedisTemplate")
@SuppressWarnings("all")
public RedisTemplate<String,Object> getShiroRedisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> template = new RedisTemplate<String,Object>();
template.setConnectionFactory(factory);
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.afterPropertiesSet();
return template;
}
}
- 自定义认证器,只需要继承:
ModularRealmAuthenticator
选择重写的认证方法即可
/**
* 根据认证策略,对多个realm进行认证
* 主要是在多realm环境下,方便区分到底是哪个realm认证失败
*
*/
@Slf4j
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
AuthenticationStrategy authenticationStrategy = this.getAuthenticationStrategy();
AuthenticationInfo authenticationInfo = authenticationStrategy.beforeAllAttempts(realms, token);
Iterator var5 = realms.iterator();
while (var5.hasNext()) {
Realm realm = (Realm) var5.next();
authenticationInfo = authenticationStrategy.beforeAttempt(realm, token, authenticationInfo);
if (realm.supports(token)) {
AuthenticationInfo info = null;
Throwable t = null;
try {
info = realm.getAuthenticationInfo(token);
} catch (Throwable throwable) {
t = throwable;
if (log.isDebugEnabled()) {
log.warn("Realm [{}] threw an exception during a multi-realm authentication attempt: {}",realm.getName(), t);
}
}
authenticationInfo = authenticationStrategy.afterAttempt(realm, token, info, authenticationInfo, t);
} else {
log.warn("Realm: [{}] does not support Token: [{}]", realm.getName(), token);
}
}
authenticationInfo = authenticationStrategy.afterAllAttempts(token, authenticationInfo);
return authenticationInfo;
}
}
- shiro session管理器,可防止每次请求频繁访问redis
public class ShiroSessionManager extends DefaultWebSessionManager {
/**
* 防止每次请求对redis高达十几次的访问
* 所以将用户sessionId存储到request请求中
* 登录成功后每一次访问都从request中取
*
*/
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
Serializable sessionId = getSessionId(sessionKey);
if (null != request && null != sessionId) {
Object sessionObj = request.getAttribute(sessionId.toString());
if (sessionObj != null) {
return (Session) sessionObj;
}
}
Session session = null;
try {
session = super.retrieveSession(sessionKey);
} catch (UnknownSessionException e) {
return null;
}
if (null != request && null != sessionId) {
request.setAttribute(sessionId.toString(), session);
}
return session;
}
}
- ShiroConfig配置类
/**
* @author itoyoung
* @date 2019-06-17 16:10
*/
@Configuration
public class ShiroConfig {
@Resource
private PasswordRealm passwordRealm;
@Resource
private PhoneSmsCodeRealm phoneSmsCodeRealm;
@Resource
private RedisSessionDAO redisSessionDAO;
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//配置不会被拦截的链接 顺序判断;
//<!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/logout", "anon");
filterChainDefinitionMap.put("/**", "authc");
// 未登录跳转的接口
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/notLogin");
Map<String, Filter> filters = new HashMap<>(10);
// ErpShiroFilter erpShiroFilter = new ErpShiroFilter();
// filters.put("authc", erpShiroFilter);
// 未授权跳转的页面
// shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setFilters(filters);
return shiroFilterFactoryBean;
}
/**
* 自定义sessionManager
*
* @param simpleCookie
* @return
*/
@Bean("sessionManager")
public SessionManager sessionManager(@Qualifier("sessionIdCookie") SimpleCookie simpleCookie) {
ShiroSessionManager sessionManager = new ShiroSessionManager();
//全局会话超时时间(单位毫秒),默认30分钟 暂时设置为12h
sessionManager.setGlobalSessionTimeout(1000 * 60 * 60 * 12);
//是否开启删除无效的session对象 默认为true
sessionManager.setDeleteInvalidSessions(true);
//配置监听session,缓存包含用户信息的session到redis中
sessionManager.setSessionDAO(redisSessionDAO);
//是否开启定时调度器进行检测过期session 默认为true
sessionManager.setSessionValidationSchedulerEnabled(true);
//设置session失效的扫描时间, 清理用户直接关闭浏览器造成的孤立会话 默认为 1个小时
//设置该属性 就不需要设置 ExecutorServiceSessionValidationScheduler
//底层也是默认自动调用ExecutorServiceSessionValidationScheduler
sessionManager.setSessionValidationInterval(1000 * 60 * 30);
//配置保存sessionId的cookie
sessionManager.setSessionIdCookie(simpleCookie);
//取消url 后面的 JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**
* 给shiro的sessionId默认的JSSESSIONID名字改掉,
* 下边的名字不能变,必须要与主项目的相同
*
* @return
*/
@Bean("sessionIdCookie")
public SimpleCookie getSessionIdCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName("SHIROSESSIONID");
//如果在Cookie中设置了"HttpOnly"属性,那么通过程序(JS脚本、Applet等)将无法读取到Cookie信息,这样能有效的防止XSS攻击。
simpleCookie.setHttpOnly(true);
//cookie有效时间
simpleCookie.setMaxAge(-1);
return simpleCookie;
}
/**
* 这里把 前面两个bean 传入到 manager中
*
* @param sessionManager
* @param abstractAuthenticator
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(SessionManager sessionManager, AbstractAuthenticator abstractAuthenticator) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> realms = new ArrayList<>();
realms.add(passwordRealm);
realms.add(phoneSmsCodeRealm);
securityManager.setRealms(realms);
//验证策略
securityManager.setAuthenticator(abstractAuthenticator);
// 自定义session管理 使用redis
securityManager.setSessionManager(sessionManager);
return securityManager;
}
/**
* 认证器 把我们的自定义验证加入到认证器中
*/
@Bean
public AbstractAuthenticator abstractAuthenticator() {
// 自定义模块化认证器,用于解决多realm抛出异常问题
// 开始没用自定义异常问题,发现不管是账号密码错误还是什么错误
// shiro只会抛出一个AuthenticationException异常
ModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator();
// 认证策略:
// AtLeastOneSuccessfulStrategy(默认): 至少一个realm认证通过就算成功
// AllSuccessfulStrategy: 所有realm认证通过就算成功
// FirstSuccessfulStrategy: 第一个realm认证通过就算成功
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
//认证realm集合
List<Realm> realms = new ArrayList<>();
realms.add(passwordRealm);
realms.add(phoneSmsCodeRealm);
authenticator.setRealms(realms);
return authenticator;
}
/**
* Shiro生命周期处理器
*/
@Bean(name = "lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
creator.setProxyTargetClass(true);
return creator;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
- 登陆测试
@RestController
public class LoginController {
@Resource
private IUserService userService;
@Resource
private RedisUtil redisUtil;
@PostMapping("/login")
public Result login(LoginQuery query) {
String phone = query.getPhone();
User user = userService.getUserByPhone(phone);
Result result = new Result();
if (null == user) {
return result.result(false, "1001", "用户不存在");
}
if (user.getStatus().equals("N")) {
return result.result(false, "1001", "用户已失效");
}
if (!phone.equals(user.getPhone())) {
return result.result(false, "1001", "用户信息错误");
}
//退出登录
SecurityUtils.getSubject().logout();
AuthenticationToken token = null;
String clientType = query.getClientType();
switch (query.getLoginType()) {
case "password":
token = new CustomUsernamePasswordToken(phone, query.getPassword(), true, clientType);
break;
case "smsCode":
String smsCode = (String) redisUtil.get(RedisConstants.SMS_CODE + query.getPhone());
//测试环境
if (!Arrays.asList("123456", smsCode).contains(query.getSmsCode())) {
return result.result(false, "1001", "验证码错误错误");
}
token = new PhoneSmsCodeToken(phone, clientType, true);
break;
default:
return result.result(false, "1001", "参数异常,登陆失败");
}
SecurityUtils.getSubject().login(token);
return result.defaultSuccess(user);
}
/**
* 未登录,shiro应重定向到登录界面,此处返回未登录状态信息由前端控制跳转页面
*
* @return
*/
@GetMapping(value = "/notLogin")
public Result notLogin() {
return new Result().result(false, "1001", "用户未登录");
}
@GetMapping(value = "/logout")
public Result logout() {
SecurityUtils.getSubject().logout();
return new Result().result(true, "200", "退出成功");
}
@GetMapping("/smsCode")
public Result getSmsCode(String phone) {
String smsCode = RandomStringUtils.randomNumeric(6);
redisUtil.set(RedisConstants.SMS_CODE + phone, smsCode, 300);
return new Result().defaultSuccess(smsCode);
}
}
@Data
public class LoginQuery implements Serializable {
private String phone;
private String password;
private String smsCode;
/**
* 登录方式:
* 账号密码:password
* 手机验证码:smsCode
*/
private String loginType = "password";
/**
* 客户端类型 :app、web
*/
private String clientType = "web";
}