需求:基于若依框架进行开发,根据业务需求实现相应的登录功能。
(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
二、过滤器:
(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("登录失败");
}