通过若依来实现多端登录功能(分表) spring security实现

3,820 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

概述: 若依是一个后台管理系统,现在我们要在若依的基础上添加一个客户端app,那么我们要实现客户端登录(分表),把app客户端的逻辑集成进去;客户端登录是通过手机号与短信验证码实现的,而若依是通过用户名密码进行验证的; 我们的思路是这样的,若依的用户名我们可以用手机号充当,若依的密码我们用手机验证码进行充当; 改动的地方有2个地方; 分别是:

第一处改动 image.png

第二处改动 验证码形式,我们返回的密码(验证码)是从redis中进行查询的

image.png 若依的密码是从配置文件中进行读取的:

image.png

一、模仿若依的 UserDetailsServiceImpl类实现UserDetailsService接口

若依的接口实现为:

package com.ruoyi.framework.web.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.service.ISysUserService;

/**
 * 用户验证处理
 *
 * @author ruoyi
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;
    
    @Autowired
    private SysPasswordService passwordService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
    {

        System.out.println("调用后台用户认证");

        SysUser user = userService.selectUserByUserName(username);

        if (StringUtils.isNull(user))
        {
            log.info("登录用户:{} 不存在.", username);
            throw new ServiceException("登录用户:" + username + " 不存在");
        }
        else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            log.info("登录用户:{} 已被删除.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            log.info("登录用户:{} 已被停用.", username);
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }

        passwordService.validate(user);

        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user)
    {
        return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
    }
}

image.png

我们的接口实现为:

package com.ruoyi.framework.web.service;

import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.ClientLoginUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.client.ClientUser;
import com.ruoyi.system.service.ISysUserService;
import com.ruoyi.system.service.impl.client.ClientUserServiceImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Collections;

/**
 * 客户端用户验证处理
 *
 * @author ruoyi
 */
@Service
public class ClientUserDetailsServiceImpl implements UserDetailsService
{
    private static final Logger log = LoggerFactory.getLogger(ClientUserDetailsServiceImpl.class);

    @Autowired
    private ClientUserServiceImpl clientUserService;


    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {

        System.out.println("调用手机客户端登录逻辑");

        ClientUser clientUser = clientUserService.exitsPhoneUser(phone);


        if (UserStatus.DELETED.getCode().equals(clientUser.getTombstone()+""))
        {
            log.info("登录用户:{} 已被删除.", phone);
            throw new ServiceException("对不起,您的账号:" + phone + " 已被删除");
        }
        else if (UserStatus.DISABLE.getCode().equals(clientUser.getState()+""))
        {
            log.info("登录用户:{} 已被停用.", phone);
            throw new ServiceException("对不起,您的账号:" + phone + " 已停用");
        }
        return createClientLoginUser(clientUser);
    }

    public UserDetails createClientLoginUser(ClientUser clientUser)
    {
        return new ClientLoginUser(clientUser.getPhone());
    }
}

二、模仿若依实现用户名密码验证

这里我们直接继承若依的用户名密码验证即可

package com.ruoyi.framework.config.security;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class ClientUsernamePasswordAuthenticationToken extends UsernamePasswordAuthenticationToken {
    public ClientUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public ClientUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(principal, credentials, authorities);
    }
}

说明:标红的地方是改动的地方

image.png

clientUserLogin方法是我们模仿若依的登录写的,login方法是若依自己写的。
/**
 * 客户端登录验证
 *
 * @param phone 手机用户名
 * @param password 密码
 * @param code 验证码
 * @param uuid 唯一标识
 * @return 结果
 */
public String clientUserLogin(String phone, String password, String code, String uuid)
{
    //密码优化
    // 用户验证
    Authentication authentication = null;
    try
    {
            ClientUsernamePasswordAuthenticationToken authenticationToken = new ClientUsernamePasswordAuthenticationToken(phone, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用ClientUserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
    }
    catch (Exception e)
    {
        e.printStackTrace();
        if (e instanceof BadCredentialsException)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
        else
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        }
    }
    finally
    {
        AuthenticationContextHolder.clearContext();
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(phone, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
    ClientLoginUser clientLoginUser = (ClientLoginUser) authentication.getPrincipal();
    //记录登录信息
   recordLoginInfo(clientLoginUser.getUsername());

    // 生成token
    return clientTokenService.createToken(clientLoginUser);
}

/**
 * 后台登录验证
 * 
 * @param username 用户名
 * @param password 密码
 * @param code 验证码
 * @param uuid 唯一标识
 * @return 结果
 */
public String login(String username, String password, String code, String uuid)
{
    boolean captchaEnabled = configService.selectCaptchaEnabled();
    // 验证码开关
    if (captchaEnabled)
    {
        validateCaptcha(username, code, uuid);
    }
    // 用户验证
    Authentication authentication = null;
    try
    {

            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);


    }
    catch (Exception e)
    {
        if (e instanceof BadCredentialsException)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }
        else
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
            throw new ServiceException(e.getMessage());
        }
    }
    finally
    {
        AuthenticationContextHolder.clearContext();
    }
    AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    recordLoginInfo(loginUser.getUserId());
    // 生成token
    return tokenService.createToken(loginUser);
}

三、模仿若依实现 UserDetails

3.1 若依实现

image.png

3.2 我们实现客户端的

image.png

package com.ruoyi.common.core.domain.model;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.SecurityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/*
*
* 客户端用户登录 用户及密码校验
* */
public class ClientLoginUser implements UserDetails {

    @Autowired
    RedisCache redisCache;
    /*
    * 用户手机号/客户独断登录名
    * */
    private String phone;
    /*
    * token
    * */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    public Long getLoginTime() {
        return loginTime;
    }

    public void setLoginTime(Long loginTime) {
        this.loginTime = loginTime;
    }

    public Long getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(Long expireTime) {
        this.expireTime = expireTime;
    }

    public String getIpaddr() {
        return ipaddr;
    }

    public void setIpaddr(String ipaddr) {
        this.ipaddr = ipaddr;
    }

    public String getLoginLocation() {
        return loginLocation;
    }

    public void setLoginLocation(String loginLocation) {
        this.loginLocation = loginLocation;
    }

    public String getBrowser() {
        return browser;
    }

    public void setBrowser(String browser) {
        this.browser = browser;
    }

    public String getOs() {
        return os;
    }

    public void setOs(String os) {
        this.os = os;
    }

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 浏览器类型
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;


    public String getPhone() {
        return phone;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    @Override
    public String getPassword() {
        ClientLoginUser clientLoginUser = (ClientLoginUser) SecurityUtils.getAuthentication().getPrincipal();
        Object cacheObject = redisCache.getCacheObject(Constants.PHONE_VERIFY_PREFIX+clientLoginUser.getUsername());
        String code2 = String.valueOf(cacheObject);
        return code2;
    }

    @Override
    public String getUsername() {
        return this.phone;
    }

    public ClientLoginUser(String phone) {
        this.phone = phone;
    }

    //------------------------------------

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }


}

四、修改 securityConfig配置文件

image.png