springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线

参考文章:
单点登录:https://mp.weixin.qq.com/s/DGFFPl93kZxS5G_DSFTBDA
多端登录:https://blog.csdn.net/zhourenfei17/article/details/88826911

一、前言

最近一个springboot项目要求增加一些app的功能。然后首先要改的就是这个登录的功能。我原本的登录就只是web端登录,实现了单点登录,就是同一个用户只能同时登录一次,如果再次登录的话,会清除上一次的登录信息。(具体实现参考上面的单点登录链接)

现在加了app的话,那就得实现app和web可以同时在线,如果登录了app,web就强制下线,或者登录了web,app就强制下线的,这用户体验很不好,所以得在单点登录的基础上加上多端同时在线。

区分是app登录还是pc登录,就是通过User-Agent来区分的。这个User-Agent可以和前端约定好,比如如果是移动端的登录请求的话,User-Agent的值就是mobile,如果是PC端的请求的话,User-Agent的值就是web。
在这里插入图片描述

我一开始想的是这两个终端登录的时候,分别存储不同的sessionID,比如app登录,生成的token,前缀带有mobile,web登录,生成的token前缀带有web。

在踢人的时候,根据上次登录的token,获取到前缀,去和当前登录的登录类型进行对比,如果一致说明是同一终端,就把上次的登录信息清除;如果不一致,说明不是同一终端,对上一次的登录不进行操作。

我生成sessionid用的是自定义的SessionId生成器,然后在shiroConfig里配置。

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import java.io.Serializable;

/**
 * 自定义SessionId生成器
 */
public class ShiroSessionIdGenerator implements SessionIdGenerator {
    /**
     * 实现SessionId生成
     */
    @Override
    public Serializable generateId(Session session) {
        Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
        //在这里生成sessionid的时候,在"login_token_"这个token前缀前面,再加个登录类型的前缀
        //比如:mobile_login_token_ 或者 web_login_token_
        return String.format("login_token_%s", sessionId);
    }
}

可是我发现,我在这里一时找不到办法来获取登录类型,那这种通过sessionid的方式来区分终端的办法就行不通。后面我找了另一种方法,就是通过定义不同登录方式的Realm来进行区分,就不对sessionid进行其他处理了。

二、整合shiro和redis

1、pom.xml引入依赖

<!-- AOP依赖,一定要加,否则权限拦截验证不生效 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- ...... -->
<!-- Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- Shiro 核心依赖 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>
<!-- Shiro-redis插件 -->
<dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>3.1.0</version>
</dependency>

2、application.yml相关配置

spring:
  # 配置Redis
  redis:
    host: localhost
    port: 6379
    timeout: 6000 #以秒为单位
    password: 123456
    database: 0
    lettuce:
      pool:
        max-active: -1
        max-wait: -1
        max-idle: 16
        min-idle: 8

3、新增相应的配置类,工具类

shiro授权和身份认证的话,因为要区分移动端和pc端,所以另外多加两个分别验证移动端和PC端的Realm。

(1)、ShiroRealm类

import org.apache.shiro.authc.*;
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.HashSet;
import java.util.List;
import java.util.Set;

/**
 * 统一的Shiro权限匹配和账号密码匹配
 */
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuService menuService;

    /**
     * 授权权限
     * 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principalCollection.getPrimaryPrincipal();
        //这里可以进行授权和处理
        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查询角色和权限(这里根据业务自行查询)
        List<Role> roleList = roleService.selectRoleByUserId(user);
        for (Role role:roleList) {
            rolesSet.add(role.getRoleName());
            List<Menu> menuList = menuService.selectMenuByRoleId(role.getRoleId());
            for (Menu menu :menuList) {
                permsSet.add(menu.getPerms());
            }
        }
        //将查到的权限和角色分别传入authorizationInfo中
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        return authorizationInfo;
    }

    /**
     * 身份认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    	//因为具体的身份认证会在移动端和PC端的Realm类中实现,这里不进行处理
        return null;
    }
}

(2)、MobileShiroRealm类

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * app端登录的Realm管理
 */
public class MobileShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
	/**
     * 授权权限
     * 在ShiroRealm统一处理,这里不做处理
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }
    
	/**
     * 身份认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取用户的输入的账号.
        String loginName = (String) authenticationToken.getPrincipal();
        //通过登录名从数据库中查找 User对象,如果找到进行验证
        User user = userService.getUserByName(loginName);
        //判断账号是否存在
        if (user == null ) {
            throw new AuthenticationException();
        }
        //状态为2表示已删除(或者已锁定,根据自己的用户表的实际情况而定)
        if ("2".equals(user.getStatus())){
            throw new AuthenticationException();
        }
        //进行验证
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //用户名
                user.getPassword(),                    //密码
                ByteSource.Util.bytes(user.getSalt()),//密码盐值
                getName()
        );
        //验证成功开始踢人(清除缓存和Session)
        ShiroUtils.deleteCache(loginName,"mobile");
        return authenticationInfo;
    }
}

(3)、WebShiroRealm类

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * web端登录的Realm管理
 */
public class WebShiroRealm extends AuthorizingRealm {
    @Autowired
    private UserService userService;
	/**
     * 授权权限
     * 在ShiroRealm统一处理,这里不做处理
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

	/**
     * 身份认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取用户的输入的账号.
        String loginName = (String) authenticationToken.getPrincipal();
        //通过登录名从数据库中查找 User对象,如果找到进行验证
        User user = userService.getUserByName(loginName);
        //判断账号是否存在
        if (user == null ) {
            throw new AuthenticationException();
        }
        //状态为2表示已删除
        if ("2".equals(user.getStatus())){
            throw new AuthenticationException();
        }
        //进行验证
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //用户名
                user.getPassword(),                    //密码
                ByteSource.Util.bytes(user.getSalt()),//密码盐值
                getName()
        );
        //验证成功开始踢人(清除缓存和Session)
        ShiroUtils.deleteCache(loginName,"web");
        return authenticationInfo;
    }
}

(4)、ShiroModularRealmAuthenticator类

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;

import java.util.Collection;
import java.util.HashMap;

/**
 * 自定义的Realm管理,主要针对多realm
 */
public class ShiroModularRealmAuthenticator extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 判断getRealms()是否返回为空
        assertRealmsConfigured();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm。要注意,对应的登录类型的Realm命名,必须要包含你指定的登录类型的字符。
        //比如,你指定的区分登录类型的字符串是:mobile和web,那么这两个登录的Realm类的命名,必须包含mobile或者web字符。
        HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());
        for (Realm realm : realms) {
            realmHashMap.put(realm.getName(), realm);
        }
        UsernamePasswordLoginTypeToken token = (UsernamePasswordLoginTypeToken) authenticationToken;
        // 登录类型
        String type = token.getLoginType();
        if (realmHashMap.get(type) != null) {
            return doSingleRealmAuthentication(realmHashMap.get(type), token);
        } else {
            return doMultiRealmAuthentication(realms, token);
        }
    }
}

(5)、UsernamePasswordLoginTypeToken类

import org.apache.shiro.authc.UsernamePasswordToken;

/**
 * 重写UsernamePasswordToken方法,增加登录类型(是app端还是pc端)
 */
public class UsernamePasswordLoginTypeToken extends UsernamePasswordToken {
    /**
     *登陆类型
     */
    private String loginType;

    public UsernamePasswordLoginTypeToken(String username, String password, String loginType) {
        super(username, password);
        this.loginType = loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }
}

(6)、ShiroConfig类

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Shiro配置类
 */
@Configuration
public class ShiroConfig {

    private final String CACHE_KEY = "shiro:cache:";
    private final String SESSION_KEY = "shiro:session:";

    //Redis配置
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;

    /**
     * 开启Shiro-aop注解支持
     * @Attention 使用代理方式所以需要开启代码支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    /**
     *  开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     */
    /*@Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }*/


    /**
     * Shiro基础配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 注意过滤器配置顺序不能颠倒
        // 配置过滤:不会被拦截的链接
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");

        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/uploads/**", "anon");
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/user/unauth", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        // 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl("/user/unauth");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setAuthenticator(modularRealmAuthenticator());
        // 自定义Ssession管理
        securityManager.setSessionManager(sessionManager());
        // 自定义Cache实现
        securityManager.setCacheManager(cacheManager());
        List<Realm> realms = new ArrayList<>();
        // 统一角色权限控制realm
        realms.add(shiroRealm());
        // app登录realm
        realms.add(mobileShiroRealm());
        // web登录realm
        realms.add(webShiroRealm());
        securityManager.setRealms(realms);
        return securityManager;
    }

    /**
     * 自定义的Realm管理,主要针对多realm
     */
    @Bean("myModularRealmAuthenticator")
    public ShiroModularRealmAuthenticator modularRealmAuthenticator() {
        ShiroModularRealmAuthenticator customizedModularRealmAuthenticator = new ShiroModularRealmAuthenticator();
        // 设置realm判断条件
        customizedModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return customizedModularRealmAuthenticator;
    }

    /**
     * 统一的身份验证器
     */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setName("web");
        return shiroRealm;
    }

    /**
     * app端的身份验证器
     */
    @Bean
    public MobileShiroRealm mobileShiroRealm() {
        MobileShiroRealm shiroRealm = new MobileShiroRealm();
        shiroRealm.setName(UserConstant.APP);
        //自定义的密码验证器
        shiroRealm.setCredentialsMatcher(shiroRetryLimit(cacheManager()));
        return shiroRealm;
    }
    /**
     * web端的身份验证器
     */
    @Bean
    public WebShiroRealm webShiroRealm() {
        WebShiroRealm shiroRealm = new WebShiroRealm();
        shiroRealm.setName(UserConstant.WEB);
        //自定义的密码验证器
        shiroRealm.setCredentialsMatcher(shiroRetryLimit(cacheManager()));
        return shiroRealm;
    }

    /**
     * 配置Redis管理器
     * @Attention 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        return redisManager;
    }

    /**
     * 配置Cache管理器
     * 用于往Redis存储权限和角色标识
     * @Attention 使用的是shiro-redis开源插件
     */
    @Bean("ehCacheManager")
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(CACHE_KEY);
        // 配置缓存的话要求放在session里面的实体类必须有个id标识
        redisCacheManager.setPrincipalIdFieldName("userId");
        return redisCacheManager;
    }

    /**
     * 自定义的SessionID生成器
     */
    @Bean
    public ShiroSessionIdGenerator sessionIdGenerator(){
        return new ShiroSessionIdGenerator();
    }

    /**
     * 配置RedisSessionDAO
     * @Attention 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
        redisSessionDAO.setKeyPrefix(SESSION_KEY);
        redisSessionDAO.setExpire(timeout);
        return redisSessionDAO;
    }

    /**
     * 配置Session管理器
     */
    @Bean
    public SessionManager sessionManager() {
    	//自定义的Session管理器,获取token
        ShiroSessionManager shiroSessionManager = new ShiroSessionManager();
        shiroSessionManager.setSessionDAO(redisSessionDAO());
        return shiroSessionManager;
    }

    /**
     * 自定义的密码验证器
     */
    @Bean("shiroRetryLimit")
    public ShiroRetryLimit shiroRetryLimit(CacheManager cacheManager){
        ShiroRetryLimit shiroRetryLimit = new ShiroRetryLimit(cacheManager);
        // 散列算法:这里使用SHA256算法;
        shiroRetryLimit.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
        // 散列的次数,比如散列两次,相当于 md5(md5(""));
        shiroRetryLimit.setHashIterations(SHA256Util.HASH_ITERATIONS);
        return shiroRetryLimit;
    }
}

(7)、ShiroUtils类

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.LogoutAware;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.apache.shiro.subject.support.DefaultSubjectContext;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisSessionDAO;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

/**
 * Shiro工具类
 */
public class ShiroUtils {

	/** 私有构造器 **/
	private ShiroUtils(){ }

    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

	private static RoleService roleService = SpringUtil.getBean(RoleService.class);

    /**
     * 获取当前用户Session
     */
    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }

    /**
     * 用户退出登录
     */
    public static void logout() {
        SecurityUtils.getSubject().logout();
    }

	/**
	 * 获取当前用户信息
	 */
	public static User getUserInfo() {
		return (User) SecurityUtils.getSubject().getPrincipal();
	}

    /**
     * 获取当前用户id
     */
    public static Integer getUserId() {
        return getUserInfo().getUserId();
    }

    /**
     * 删除用户缓存信息
     * @Param  loginName 用户登录名称
     * @Param  loginType 当前登录的登录方式(mobile 移动端登录,web PC端登录)
     */
    public static void deleteCache(String loginName, String loginType){
        //从缓存中获取Session
        Session session = null;
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        User sysUserEntity;
        Object attribute = null;
        for(Session sessionInfo : sessions){
            //遍历Session,找到该用户名称对应的Session
            attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }
            System.out.println(((SimplePrincipalCollection) attribute).getPrimaryPrincipal());
            sysUserEntity = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if (sysUserEntity == null) {
                continue;
            }
            if (Objects.equals(sysUserEntity.getUserName(), loginName)) {
                session=sessionInfo;
                break;
            }
        }
        if (session == null||attribute == null) {
            return;
        }
        /**
         * 根据当前登录类型,在attribute中查找上一次登录的Realm,
         *      1、如果有值,说明当前登录的终端和上一次登录的终端相同,则删除上一次登录的session(清空上一次的登录信息,重新登录)。
         *      2、如果没有值,说明当前登录的终端和上一次登录的终端不一样,不对上一次登录的session删除,实现app端和web端可同时在线
         */
        Collection collection = ((SimplePrincipalCollection) attribute).fromRealm(loginType);
        if (collection.size()>0){
            //删除session
            redisSessionDAO.delete(session);
        }
        //删除Cache,在访问受限接口时会重新授权
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        Authenticator authc = securityManager.getAuthenticator();
        ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
    }
}

4、controller类

在controller层中,通过请求头的User-Agent 来区分登录终端是移动端还是PC端,这个User-Agent可以和前端约定好,比如如果是移动端的登录请求的话,User-Agent的值就是mobile,如果是PC端的请求的话,User-Agent的值就是web。还有其他登录方式,根据自己的实际情况而定。

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authz.annotation.RequiresUser;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.*;

/**
 * 登录
 */
@RestController
@RequestMapping("/user")
public class LoginController {

    /**
     * 登录
     */
    @RequestMapping("/login")
    public ResultVo login(@RequestBody User user, HttpServletRequest request){
        Map<String,Object> map = new HashMap<>();
        //进行身份验证
        try{
            //验证身份和登陆
            Subject subject = SecurityUtils.getSubject();
            //获取登录的终端
            String loginType = request.getHeader("User-Agent");
            UsernamePasswordLoginTypeToken token = new UsernamePasswordLoginTypeToken(user.getUserName(), user.getPassword(), loginType);
            //进行登录操作
            subject.login(token);
        }catch (IncorrectCredentialsException e) {
            return ResultVoUtil.error(1000,e.getMessage());
        } catch (LockedAccountException e) {
            return ResultVoUtil.error(1004,e.getMessage());
        } catch (AuthenticationException e) {
            return ResultVoUtil.error(ResultEnum.USER_NOT_ERROR);
        } catch (Exception e) {
            return ResultVoUtil.error(ResultEnum.UNKNOWN_EXCEPTION);
        }
        map.put("code",0);
        map.put("msg","登录成功");
        map.put("token", ShiroUtils.getSession().getId().toString());
        return ResultVoUtil.success(map);
    }
    /**
     * 未登录
     */
    @ApiOperation("未登录")
    @RequestMapping("/unauth")
    public ResultVo unauth(){
        return ResultVoUtil.error(ResultEnum.USER_NOT_LOGIN);
    }

    /**
     * 登出
     */
    @ApiOperation("退出登录")
    @RequestMapping("/logout")
    @RequiresUser
    @Log(operMethod = "退出登录",operInfo = "退出登录")
    public ResultVo logout(){
        //登出Shiro会帮我们清理掉Session和Cache
        ShiroUtils.logout();
        return ResultVoUtil.success(0,"退出登录成功");
    }
}

文中还用到的一些其他配置类和工具类,完整的可以看我开头放的单点登录的那篇链接,这里我是在这篇链接上的单点登录的基础上,再加了移动端和PC端同时在线:如果登录终端是同一个的话,就会踢掉上一次的登录,不同的话,则多个终端可同时在线。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个用于快速开发Java应用程序的开源框架,Shiro是一个强大且易于使用的Java安全框架,Redis是一个开源的内存数据库。结合使用这些技术可以实现单点登录功能。 在Spring Boot中使用Shiro来处理认证和授权,可以通过配置Shiro的Realm来实现用户的登录认证和权限控制。将用户的信息存储在Redis中,利用Redis的持久化特性来实现用户登录状态的共享和存储。 首先,在Spring Boot项目的配置文件中配置Redis的连接信息,以便连接到Redis数据库。 然后,创建一个自定义的Shiro的Realm,在其中重写认证和授权的方法。在认证方法中,将用户的登录信息存储到Redis中,以便其他服务可以进行验证。在授权方法中,根据用户的角色和权限进行相应的授权操作。 接着,在Spring Boot项目的配置类中配置Shiro的相关设置,包括Realm、Session管理器、Cookie管理器等。 最后,可以在Controller层中使用Shiro的注解来标记需要进行认证和授权的接口,以确保只有登录后且具备相应权限的用户才能访问这些接口。 总的来说,通过使用Spring Boot、ShiroRedis的组合,可以实现单点登录的功能。用户在登录后,将登录信息存储到Redis中,其他服务可以通过验证Redis中的数据来判断用户的登录状态。同时,Shiro提供了强大的认证和授权功能,可以确保只有具备相应权限的用户才能访问受保护的接口。这些功能的具体实现可以通过深入研究Spring Boot、ShiroRedis的源码来了解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值