SpringBoot2.x整合Shiro

前言

  • 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";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值