需求:基于若依框架进行开发,根据业务需求实现相应的登录功能。

(1)系统用户账号密码登录
(2)非系统用户账号密码登录
(3)系统用户手机短信验证码登录
(4)系统用户微信登录
(5)系统用户保存登录状态自动登录

一、SpringSecurity介绍

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。

它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

重要核心功能:用户认证Authentication、用户授权Authorization
用户认证:通过校验用户名和密码来判断,用户是否能访问该系统。
用户授权:验证用户是否拥有操作的权限。
本质:过滤器链路,含有很多过滤器(15个)。责任链设计模式。

1.使用依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

2.认证流程

来源自博客https://blog.csdn.net/weixin_45974277/article/details/123115966
spring security认证流程-苏七

二、过滤器:

(1)核心过滤器链

请求|响应
|

  • UsernamePasswordAuthenticationFilter:拦截/login的Post请求,校验用户名密码
  • ExceptionTranslationFilter:异常过滤器,处理认证授权中跑出的异常
  • FilterSecurityInterceptor:方法级权限过滤器

|
api接口

(2)过滤器加载流程:

DelegatingFilterProxy:SpringSecurity配置过滤器

  • doFilter:初始化
  • initDelegate:DelegatingFilterProxy根据targetBeanName从Spring 容器中获取被注入到Spring 容器的Filter实现类,在DelegatingFilterProxy配置时一般需要配置属性targetBeanName

(3)过滤器简介

WebAsyncManagerIntegrationFilter:此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager。
实现安全上下文从调用者线程 到被调用者线程的传播。

三、使用微信相关

1.使用依赖:

    <!-- 微信小程序 -->
    <dependency>
        <groupId>com.github.binarywang</groupId>
        <artifactId>weixin-java-miniapp</artifactId>
        <version>4.3.0</version>
    </dependency>

    <dependency>
        <groupId>com.github.binarywang</groupId>
        <artifactId>weixin-java-mp</artifactId>
        <version>4.3.0</version>
    </dependency>

2.微信参数类

@Data
@ConfigurationProperties(prefix = "wx")
public class WxMaProperties {

    /**
     * redis 配置
     */
    private RedisConfig redisConfig;

    @Data
    public static class RedisConfig {

        private String host;	// redis服务器 主机地址
        private Integer port;	// redis服务器 端口号
        private String password;	// redis服务器 密码
        private Integer timeout = 500;	// redis 服务连接超时时间

        /**
         * 数据库.
         */
        private int database = 0;
        private Integer maxActive;
        private Integer maxIdle;
        private Integer maxWaitMillis;
        private Integer minIdle;
        
        // 前缀
        private String keyPrefix = "wx";
    }

    private List<MaConfig> configs;

    @Data
    public static class MaConfig {
        private String appid;	// 设置微信小程序的appid
        private String secret;	// 设置微信小程序的Secret
        private String token;	// 设置微信小程序消息服务器配置的token
        private String aesKey;	// 设置微信小程序消息服务器配置的EncodingAESKey
        private String msgDataFormat;	// 消息格式,XML或者JSON
    }
}

3.微信配置类

@Slf4j
@AllArgsConstructor
@Configuration
@EnableConfigurationProperties(WxMaProperties.class)
public class WxMaConfiguration {

    private final WxMaProperties properties;

    @Bean
    public WxMaService wxMaService() {
        final List<WxMaProperties.MaConfig> configs = this.properties.getConfigs();
        if (configs == null) {
            throw new RuntimeException("未添加小程序相关配置,请核实!");
        }
        WxMaService service = new WxMaServiceImpl();
        WxMaProperties.RedisConfig redisConfig = this.properties.getRedisConfig();
        service.setMultiConfigs(configs
                .stream().map(a -> {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    JedisPool jedisPool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
                            redisConfig.getTimeout(), redisConfig.getPassword(),redisConfig.getDatabase());
                    WxMaRedisConfigImpl config = new WxMaRedisConfigImpl(jedisPool);
                    config.setAppid(a.getAppid());
                    config.setSecret(a.getSecret());
                    config.setToken(a.getToken());
                    config.setAesKey(a.getAesKey());
                    config.setMsgDataFormat(a.getMsgDataFormat());
                    config.setRedisKeyPrefix(redisConfig.getKeyPrefix());
                    return config;
                }).collect(Collectors.toMap(WxMaDefaultConfigImpl::getAppid, a -> a, (o, n) -> o)));
        return service;
    }
}

4.根据code获得openId

public String getOpenId(WxAuthDTO wxAuthDTO) throws WxErrorException {
        if(Objects.isNull(wxAuthDTO.getAppId())){
            // 配置需要改动
            wxAuthDTO.setAppId(configService.selectConfigByKey("wx.appid"));
        }
        WxMaService wxMaService = this.wxMaService.switchoverTo(wxAuthDTO.getAppId());

        WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(wxAuthDTO.getCode());

        return  sessionInfo.getOpenid();
    }

四、代码示例

1.登录对象:

@Data
public class LoginBody {

    private String username; 	//用户名
    private String password;	// 用户密码
    private String code;	// 图形验证码计算结果
    private String uuid = "";	// 图形验证码唯一标识
    
    // >>>>>>>>>>>>>>>>>>>>>>>> 微信相关业务入参 >>>>>>>>>>>>>>>>>>>>>>>>
    private Boolean isSave;		// 是否保存登录状态30天
    private Boolean isChange;	// 是否同意换绑微信openId
    private Boolean isWxLogin;	//是否是微信登录
    // 微信登录入参 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>   
    private String wxCode;	// 微信登录 code
    private String iv;	// 微信登录 iv
    private String encryptedData;	// 微信登录 encryptedData

}
/**
 * 非系统用户用户身份权限
 */
@Data
public class NotSysLoginUser extends LoginUser {
    private String password;
}

2.Security 配置

(1)SecurityConfig

根据业务需求配置不同的用户认证逻辑过滤器。

  • 自定义认证接口: 调用ProviderManager的方法进行认证 如果认证通过生成jwt令牌。
  • 自定义UserDetailsService:在这个实现类中去查询数据库。
  • 配置加密算法BCryptPasswordEncoder:数据库中的密码是密文存储的,在进行认证的时基于密文进行校验。在Spring容器中注入一个PasswordEncoder对象,Spring Security要求这个配置类要继承WebSecurityConfigurerAdapter。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;	// 认证失败处理类
    
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;	// 退出处理类
    
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;	// token认证过滤器
    
    @Autowired
    private CorsFilter corsFilter;	// 跨域过滤器
    
    @Autowired
    @Qualifier("UserDetailsServiceImpl")
    private UserDetailsService userDetailsService;	// 系统用户认证逻辑
    
    @Autowired
    @Qualifier("NotSystemUserDetailsServiceImpl")
    private UserDetailsService notSystemUserDetailsService;	// 非系统用户认证逻辑

    @Autowired
    @Qualifier("SmsUserDetailsServiceImpl")
    private UserDetailsService smsUserDetailsService;	// 系统用户短信验证认证逻辑

    /**
     * 解决 无法直接注入 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 非系统用户端登录
       NotSystemUserAuthenticationProvider notSystemUserAuthenticationProvider = new NotSystemUserAuthenticationProvider();
       notSystemUserAuthenticationProvider.setUserDetailsService(notSystemUserDetailsService);

        // 系统用户短信验证登录
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(smsUserDetailsService);

        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                .antMatchers("/login",
                        "/**/smsLogin",
                        "/**/appletsLogin",
                        "/**/wxLogin").anonymous()
                .antMatchers(
                        HttpMethod.GET,
                        "/",
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js",
                        "/profile/**"
                ).permitAll()
                .antMatchers("/doc.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter 跨域过滤器
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        
        // >> 添加自定义登录过滤器
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class).authenticationProvider(notSystemUserAuthenticationProvider);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class).authenticationProvider(smsCodeAuthenticationProvider);
    }

    /**
     * 强散列哈希加密实现
     * 
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 重写校验用户名密码方法
        auth.authenticationProvider(new ValidPwdAuthenticationProvider(userDetailsService));
        // 原校验方法
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

(2)重写校验用户名密码方法

在根据手机号、微信openId等方法查询到用户信息时,获取到的用户密码是加密后的;而用账号密码登录时,密码是未加密的。所以需要将加密后的和未加密的都与数据库中的密码(加密的)进行比对。

/**
 * 账号加密密码鉴权 Provider,要求实现 DaoAuthenticationProvider 接口
 *
 */
@Component
public class ValidPwdAuthenticationProvider extends DaoAuthenticationProvider {

    public UserNameAuthenticationProvider(@Qualifier("UserDetailsServiceImpl") UserDetailsService userDetailsService) {
        super();
        setUserDetailsService(userDetailsService);
    }

	// 重写校验用户名密码方法 additionalAuthenticationChecks
    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        String presentedPassword = (String) authentication.getCredentials();
        if (userDetails == null || authentication.getCredentials() == null) {
            throw new BadCredentialsException("用户名或密码错误");
        } else if (!new BCryptPasswordEncoder().matches(presentedPassword, userDetails.getPassword())
                && !userDetails.getPassword().equals(presentedPassword)) {
            // 将加密过的密码和未加密过的密码都进行匹配,都不相等则返回错误
            throw new BadCredentialsException("用户名或密码错误");
        }
    }
}

(3)认证逻辑

不同的登录方式入参不同,需要实现不同的查询逻辑。

a.系统用户:账号密码登录【基础】

在Spring Security的整个认证流程中,会调用UserDetailsService中的loadUserByUsername方法,根据用户名称查询用户数据。默认情况下调用的是InMemoryUserDetailsManager中的方法,该UserDetailsService是从内存中获取用户的数据。需要从数据库中获取用户的数据,那么此时就需要自定义一个UserDetailsService来覆盖默认的配置。

/**
 * 加载用户特定数据的核心接口。
 * UserDetails: 提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
 */
@Service("UserDetailsServiceImpl")
public class UserDetailsServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByUserName(username);
        // 可对user进行校验
        // eg:xxx用户不存在/已停用/已删除...
        return createLoginUser(user);
    }
    
    public UserDetails createLoginUser(SysUser user) {
        LoginUser loginUser = new LoginUser(user.getUserId(), user, permissionService.getMenuPermission(user));
        
        // 1.可对其他参数进行校验
        // OtherObject otherObject = xxxMapper.selectById(user.getOhterId);
        // if (otherObject == null) {
        	// throw new ServiceException("other对象不存在");
        // }
        
		// 2.可设置loginUser参数
		// eg:loginUser.setCustomerParam("自定义字段值");
        return loginUser;
    }
}
b.非系统用户:账号密码登录(与系统用户在不同的表中)
/**
 * 非系统用户验证处理
 */
@Service("NotSystemUserAuthenticationServiceImpl")
public class NotSystemUserAuthenticationServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(NotSystemUserAuthenticationServiceImpl.class);

    @Autowired
    private NotSystemUserService notSystemUserService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    	// 对非系统用户进行校验
        NotSystemUser user = notSystemUserService.selectNotSystemUserByUserAccount(userName);
        if (StringUtils.isNull(user)) {
            throw new ServiceException("登录用户:" + userName + " 不存在");
        }

        return createLoginUser(user);
    }

    private UserDetails createLoginUser(HnppUserStaff user) {
    	// 构造非系统用户对象
        NotSysLoginUser loginUser = new AppLoginUser();
        loginUser.setUserId(Long.valueOf(user.getStaffId()));
        loginUser.setExternalUser(true);
		// ...
        return loginUser;
    }
}
public class NotSystemUserAuthenticationProvider extends DaoAuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        NotSystemUserAuthenticationToken authenticationToken = (NotSystemUserAuthenticationToken) authentication;

		// 获取手机号
        String telephone = (String) authenticationToken.getPrincipal();
		
		// 根据手机号,查询用户基础信息
        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);

        if (authentication.getCredentials() == null) {
            this.logger.debug("Failed to authenticate since no credentials provided");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        if (!SecurityUtils.matchesPassword(presentedPassword, userDetails.getPassword())) {
            this.logger.debug("Failed to authenticate since password does not match stored value");
            throw new BadCredentialsException(this.messages
                    .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        }

        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        NotSystemUserAuthenticationToken authenticationResult = new NotSystemUserAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 NotSystemUserAuthenticationToken 的子类或子接口
        return NotSystemUserAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}
/**
 * 模仿 UsernamePasswordAuthenticationToken 实现
 */
public class NotSystemUserAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	 /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码(账号)
     */
    private final Object principal;

	// 密码
    private Object credentials;

    /**
     * 构建一个没有鉴权的 NotSystemUserAuthenticationToken
     */
    public NotSystemUserAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 NotSystemUserAuthenticationToken
     */
    public NotSystemUserAuthenticationToken(Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        Assert.isTrue(!isAuthenticated,
                "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }
    
    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        this.credentials = null;
    }
}
c.系统用户:短信验证(手机号)
/**
 * 短信验证处理
 *
 * @author ruoyi
 */
@Service("SmsUserDetailsServiceImpl")
public class SmsUserDetailsServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(SmsUserDetailsServiceImpl.class);

    @Autowired
    private ISysUserService userService;

    @Autowired
    private SysPermissionService permissionService;

    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        SysUser user = userService.selectUserByPhone(phone);
        if (StringUtils.isNull(user)) {
            throw new UserPasswordNotMatchException();
        }
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(SysUser user) {

        LoginUser loginUser = new LoginUser(user.getUserId(), user, permissionService.getMenuPermission(user));
        // 进行业务判断,设置loginUer值..
        return loginUser;
    }
}
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String telephone = (String) authenticationToken.getPrincipal();

        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);

        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }


    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}
/**
 * 模仿 UsernamePasswordAuthenticationToken 实现
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

3.登录方法

(1)系统用户:账号密码登录【基础】

// An highlighted block
	/**
     * 登录验证
     * 
     * @param username 用户名
     * @param password 密码
     * @param code 验证码
     * @param uuid 唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        boolean captchaOnOff = configService.selectCaptchaOnOff();
        // 验证码开关
        if (captchaOnOff)
        {
            validateCaptcha(username, code, uuid);
        }
        // 用户验证
        Authentication authentication = null;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        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());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 生成token
        return tokenService.createToken(loginUser);
    }

(2)非系统用户:账号密码登录(与系统用户在不同的表中)

/**
 * 非系统用户:登录验证
 *
 * @param userName 账号
 * @param password 密码
 * @param verifyCode 验证码
 * @param uuid 唯一标识
 * @return 结果
*/
public AjaxResult NotSystemUserLogin(String userName, String password, String verifyCode, String uuid) {
        Map<String, Object> resultMap = new HashMap<>(3);
        List<String> roles = new ArrayList<>();

        boolean captchaOnOff = configService.selectCaptchaOnOff();
        // 验证码开关
        if (captchaOnOff) {
            validateCaptcha(userName, verifyCode, uuid);
        }
        
        // 用户验证
        Authentication authentication;
        try {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            //以非系统用户的身份登录
            Authentication authenticationToken = new NotSystemUserAuthenticationToken(userName,password);
            authentication = authenticationManager
                    .authenticate(authenticationToken);
            resultMap.put("loginType", UserConstants.USER_TYPE_NOT_SYS_USER);
            roles.add(UserConstants.USER_TYPE_SUPERVISED_PERSON);
        } catch (Exception e) {
            e.printStackTrace();
            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());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(userName, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        resultMap.put("roles",roles);
        resultMap.put("token", tokenService.createToken(loginUser));
        // 生成token
        return AjaxResult.success(resultMap);
}

(3)系统用户:手机短信登录

/**
 * 手机短信登录验证
 * 增加保存登录状态功能
 * @return 结果
 */
public AjaxResult smsLogin2(SmsLoginBody smsLoginBody) {
		String phoneNo = smsLoginBody.getPhoneNo();
	    String smsCode = smsLoginBody.getSmsCode();
        String verifyCode = smsLoginBody.getVerifyCode();
        String uuid = smsLoginBody.getUuid();

        // 用户验证
        Authentication authentication;
        try  {
			// 取得发送的短信验证码,判断是否过期或者与传入的不等..
            String smsCodeInCache = "sendCodeValue";
            if( StringUtils.isNotBlank(smsCodeInCache) || smsCodeInCache.equals(smsCode) ) {
                // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
                Authentication  authenticationToken = new SmsCodeAuthenticationToken(phoneNo);
                authentication = authenticationManager
                        .authenticate(authenticationToken);
            }
        } catch (Exception e)  {
            e.printStackTrace();
            if (e instanceof BadCredentialsException) {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNo, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            } else {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNo, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
      
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(phoneNo, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();

        // 生成token
        String token = tokenService.createToken(loginUser);
        AjaxResult success = AjaxResult.success();
        success.put(Constants.TOKEN, token);
        return success;  
} 

(4)系统用户:微信登录验证

流程:前端传入wxCode -> 后端使用微信api获得用户的openId -> 根据openId查找指定用户。openId与用户是1对1的关系。

/**
 * 生成令牌
 * 系统用户-账号密码登录通用方法
 * 任何可以查询获取系统用户账号和密码的登录方式都可以调用。
 */
public String accountLogin(String username, String password) {

        // 用户验证
        Authentication authentication = null;
        try {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } 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());
            }
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();

        // 生成token
        return tokenService.createToken(loginUser);
    }
/**
  * 小程序 - 微信登录
  * 前端传入wxCode等微信登录信息
  * 1.isWxLogin = true:使用微信登录 -> 使用openId查询用户
  * 2.isWxLogin = false:使用保存登录状态登录 -> 从redis中获取userId查询用户
*/
public AjaxResult wxLogin(LoginBody loginBody) {

        AjaxResult ajax = AjaxResult.success();
        if (StringUtils.isBlank(loginBody.getWxCode()) || loginBody.getIsWxLogin() == null) {
            return AjaxResult.error("参数不为空");
        }

        try {
			// 调用微信api获取openId
            WxAuthDTO authDTO = new WxAuthDTO();
            BeanUtil.copyProperties(loginBody, authDTO);
            authDTO.setCode(loginBody.getWxCode());
            String openId = this.getOpenId(authDTO);

            if (StringUtils.isBlank(openId)) {
                return AjaxResult.error("登录失败");
            }

            if (loginBody.getIsWxLogin()) {
                // 是微信登录,使用openId查询用户
                SysUser sysUser =  sysUserMapper.selectUserByOpenId(openId);
                if (sysUser == null) {
                    return AjaxResult.error("用户不存在");
                }

                // 账号密码生成令牌
                String token = this.accountLogin(sysUser.getUserName(), sysUser.getPassword());
                ajax.put(Constants.TOKEN, token);

            } else {

                // 查询redis中是否存在保存登录信息
                Long userId = redisCache.getCacheObject(Constants.WX_LOGIN_KEY + openId);
                if (userId != null) {
                    // 根据userId查询用户
                    SysUser sysUser = sysUserMapper.selectUserById(userId);
                    if (sysUser == null) {
                        return AjaxResult.error("用户不存在");
                    }

                    // 账号密码生成令牌
                    String token = this.accountLogin(sysUser.getUserName(), sysUser.getPassword());
                    ajax.put(Constants.TOKEN, token);
                }
            }
        } catch (WxErrorException e) {
            e.printStackTrace();
            return AjaxResult.error("登录失败");
        }
        return ajax;
}  

(5)系统用户:保存登录状态n天

需要配合微信登录使用,如果勾选了保存登录状态,则绑定用户和openId的关系。下次登录时根据openId查询用户信息进行登录。如果登录时获取的openId与用户原绑定的不同,则提示是否需要换绑。账号密码登录和手机验证码登录生成令牌前均可使用。

/**
 * 系统用户
 * 勾选保存登录状态isSave=true时,才对账号openId进行绑定或改绑,需要前端传入前端传入wxCode,并在redis保存30天。
 */
LoginBody loginBody = new LoginBody();
try {
            // 如果选中了保存登录状态,记录userId在redis,且对openId进行绑定、改绑
            if (loginBody.getIsSave()) {
                if (StringUtils.isBlank(loginBody.getWxCode())) {
                    return AjaxResult.error("微信code不为空");
                }

                String openId = null;
                SysUser sysUser = userService.selectUserByUserName(loginBody.getUsername());
                if (sysUser != null) {
                    openId = sysUser.getOpenId();
                } else {
                    return AjaxResult.error("用户不存在!");
                }

                WxAuthDTO authDTO = new WxAuthDTO();
                BeanUtil.copyProperties(loginBody, authDTO);
                authDTO.setCode(loginBody.getWxCode());
                String newOpenId = this.getOpenId(authDTO);

                // 如果传入了wxCode,则查询该用户是否绑定过openId,如果没有则绑定;如果已经绑定过,判断是否改绑
                if (StringUtils.isBlank(openId)) {
                    SysUser updateOpenId = new SysUser();
                    updateOpenId.setUserId(sysUser.getUserId());
                    updateOpenId.setOpenId(newOpenId);
                    int updateRes = sysUserMapper.updateById(updateOpenId);

                } else if (StringUtils.isNotBlank(newOpenId) && !newOpenId.equals(openId)) {

                    if (loginBody.getIsChange()) {
                        // 允许换绑账号,同时将库中已存在的newOpenId去掉
                        SysUser oldOpenIdUser = sysUserMapper.selectUserByOpenId(newOpenId);
                        if (oldOpenIdUser != null) {
                            sysUserMapper.clearUserOpenId(oldOpenIdUser.getUserId());
                        }

                        SysUser updateOpenId = new SysUser();
                        updateOpenId.setUserId(sysUser.getUserId());
                        updateOpenId.setOpenId(newOpenId);
                        int updateRes = sysUserMapper.updateById(updateOpenId);
                    } else {
                        // 提示是否改绑
                        return AjaxResult.success("登录的账号绑定的微信号有变,是否换绑账号");
                    }
                }

                //把userId缓存30天 wx_login_key:openId
                redisCache.setCacheObject(Constants.WX_LOGIN_KEY + newOpenId, sysUser.getUserId(),30, TimeUnit.DAYS);
            }
        } catch (WxErrorException e) {
            e.printStackTrace();
            return AjaxResult.error("登录失败");
}

参考材料

1.spring security认证流程
Spring Security在前后端分离项目中的使用

Logo

快速构建 Web 应用程序

更多推荐