若依多表认证
若依采用的SpringSecurity 作为认证授权框架,SpringSecurity 默认只支持单表的认证授权。
如果需要多表的认证授权的,
实现步骤
1. 创建新的用户表
DROP TABLE IF EXISTS `tb_mobile_user`;
CREATE TABLE `tb_mobile_user` (
`user_id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`user_name` varchar(30) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL COMMENT '用户账号',
`nick_name` varchar(30) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '小明' COMMENT '用户昵称',
`email` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '' COMMENT '用户邮箱',
`sex` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT 'https://laf-files.oss-cn-shenzhen.aliyuncs.com/laf-images/2024/11/03/6727302f9d5432242cb05022' COMMENT '头像地址',
`password` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL DEFAULT '' COMMENT '密码',
`c_id` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '设备id , 每次的登录都需要更新',
`client_platform` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '设备平台 每次的登录都需要更新',
`status` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`del_flag` char(1) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`create_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`update_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '修改人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`user_id`) USING BTREE,
UNIQUE INDEX `uk_username`(`user_name` ASC) USING BTREE COMMENT '用户名唯一',
FULLTEXT INDEX `idx_email`(`email`)
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
2. 创建一个新模块 user-modile
在该模块中给为 AuthenticationManager 提供 loadUserByUsername 方法
/**
* 根据用户名查询用户
*
* @param userName 用户名
* @return
*/
@Override
public MobileUser selectUserByUserName(String userName) {
return baseMapper.selectUserByUserName(userName);
}
3. 在 framework.web.service 模块定义个类实现 UserDetailsService 接口
@Service("mobileUserDetailsServiceImpl")
public class MobileUserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IMobileUserService mobileUserService;
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
MobileUser mobileUser = mobileUserService.selectUserByUserName(username);
System.out.println("这是移动端用户:" + mobileUser);
if (StringUtils.isNull(mobileUser)) {
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
return createLoginUser(mobileUser);
}
}
4. 在定义的 LoginUser 类 , 注意实现 UserDetails 的方法全部返回 true ,如果需要实现类似功能,在登陆的时候和进行一系列的判断。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginMobileUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户唯一标识
*/
private String token;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录时间
*/
private Long loginTime;
/**
* 用户信息
*/
private MobileUser mobileUser;
public LoginMobileUser(Long userId, MobileUser mobileUser) {
this.userId = userId;
this.mobileUser = mobileUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
@JSONField(serialize = false)
public String getPassword() {
return mobileUser.getPassword();
}
@Override
public String getUsername() {
return mobileUser.getUserName();
}
/**
* 账户是否未过期,过期无法验证
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定用户是否解锁,锁定的用户无法进行身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的用户不能身份验证
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
}
5. 申明对应的 AuthenticationManager Bean 对象对新表的认证和授权
/**
* spring security配置
*
* @author ruoyi
*/
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
@Configuration
public class SecurityConfig {
/**
* 自定义用户认证逻辑
*/
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("mobileUserDetailsServiceImpl")
private UserDetailsService mobileUserDetailsServiceImpl;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 身份验证实现
*/
@Bean(name = "userAuthenticationManager")
@Primary // 当存在多个同样的bean对象,则通过@Primary来指定当前的bean 是主要的,如果没有明确的指定就是使用该bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService);
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
return new ProviderManager(daoAuthenticationProvider);
}
/**
* 移动端用户的认证实现
*
* @return AuthenticationManager
*/
@Bean("mobileUserAuthenticationManager")
public AuthenticationManager MemberAuthenticationManagerBean() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(mobileUserDetailsServiceImpl);
authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
return new ProviderManager(authenticationProvider);
}
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
// CSRF禁用,因为不使用session
.csrf(csrf -> csrf.disable())
// 禁用HTTP响应标头
.headers((headersCustomizer) -> {
headersCustomizer.cacheControl(HeadersConfigurer.CacheControlConfig::disable).frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin);
})
// 认证失败处理类
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
// 基于token,所以不需要session
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 注解标记允许匿名访问的url
.authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers(
"/login",
"/register",
"/user/users/login",
"/user/users/register",
"/user/users/resetPwd",
"/user/categorys/tree/list",
"/user/lostItem/wait-picked/page",
"/captchaImage",
"/register/sendSms",
"/pwd/sendSms",
"/check/smsCode",
"/common/upload",
"/common/uploads",
"/common/download"
).permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})
// 添加Logout filter
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
// 添加JWT filter
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
// 添加CORS filter
.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)
.addFilterBefore(corsFilter, LogoutFilter.class)
.build();
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
6. 修改 TokenService , 添加专门为 user-mobile 表的相关方法 , 如果的 user-mobile 表的不需要添加 Token 前缀 Bearer , 在过滤器用户与区分登录用户查询的表的
/**
* token验证处理
*
* @author ruoyi
*/
@Component
public class TokenService
{
private static final Logger log = LoggerFactory.getLogger(TokenService.class);
// 令牌自定义标识
@Value("${token.header}")
private String header;
// 令牌秘钥
@Value("${token.secret}")
private String secret;
// 令牌有效期(默认30分钟)
@Value("${token.expireTime}")
private int expireTime;
protected static final long MILLIS_SECOND = 1000;
protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
private static final Long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
@Autowired
private RedisCache redisCache;
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request)
{
// 获取请求携带的令牌
String token = getToken(request);
if (StringUtils.isNotEmpty(token))
{
try
{
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
String userKey = getTokenKey(uuid);
LoginUser user = redisCache.getCacheObject(userKey);
return user;
}
catch (Exception e)
{
log.error("获取用户信息异常'{}'", e.getMessage());
}
}
return null;
}
/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginMobileUser getLoginMobileUser(HttpServletRequest request) {
// 获取请求携带的令牌
String token = getMobileToken(request);
if (StringUtils.isNotEmpty(token)) {
try {
Claims claims = parseToken(token);
// 解析对应的权限以及用户信息
String uuid = (String) claims.get(Constants.LOGIN_MOBILE_USER_KEY);
String userKey = getTokenMobileKey(uuid);
LoginMobileUser user = redisCache.getCacheObject(userKey);
return user;
} catch (Exception e) {
log.error("获取用户信息异常'{}'", e.getMessage());
}
}
return null;
}
/**
* 设置用户身份信息
*/
public void setLoginUser(LoginUser loginUser)
{
if (StringUtils.isNotNull(loginUser) && StringUtils.isNotEmpty(loginUser.getToken()))
{
refreshToken(loginUser);
}
}
/**
* 设置用户身份信息
*/
public void setLoginMobileUser(LoginMobileUser loginMobileUser) {
if (StringUtils.isNotNull(loginMobileUser) && StringUtils.isNotEmpty(loginMobileUser.getToken())) {
refreshTokenMobile(loginMobileUser);
}
}
/**
* 删除用户身份信息
*/
public void delLoginUser(String token)
{
if (StringUtils.isNotEmpty(token))
{
String userKey = getTokenKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 删除用户身份信息
*/
public void delLoginMobileUser(String token) {
if (StringUtils.isNotEmpty(token)) {
String userKey = getTokenMobileKey(token);
redisCache.deleteObject(userKey);
}
}
/**
* 创建令牌
*
* @param loginUser 用户信息
* @return 令牌
*/
public String createToken(LoginUser loginUser)
{
String token = IdUtils.fastUUID();
loginUser.setToken(token);
setUserAgent(loginUser);
refreshToken(loginUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
/**
* 创建令牌 移动端的
*
* @param loginMobileUser
* @return
*/
public String createTokenMobile(LoginMobileUser loginMobileUser) {
String token = IdUtils.fastUUID();
loginMobileUser.setToken(token);
refreshTokenMobile(loginMobileUser);
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_MOBILE_USER_KEY, token);
return createToken(claims);
}
/**
* 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginUser
* @return 令牌
*/
public void verifyToken(LoginUser loginUser)
{
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
{
refreshToken(loginUser);
}
}
/**
* 移动端 验证令牌有效期,相差不足20分钟,自动刷新缓存
*
* @param loginMobileUser
*/
public void verifyTokenMobile(LoginMobileUser loginMobileUser) {
long expireTime = loginMobileUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
refreshTokenMobile(loginMobileUser);
}
}
/**
* 刷新令牌有效期
*
* @param loginUser 登录信息
*/
public void refreshToken(LoginUser loginUser)
{
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenKey(loginUser.getToken());
redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
}
/**
* 移动端 刷新令牌有效期
*
* @param loginMobileUser 登录信息
*/
public void refreshTokenMobile(LoginMobileUser loginMobileUser) {
loginMobileUser.setLoginTime(System.currentTimeMillis());
loginMobileUser.setExpireTime(loginMobileUser.getLoginTime() + expireTime * MILLIS_MINUTE);
// 根据uuid将loginUser缓存
String userKey = getTokenMobileKey(loginMobileUser.getToken());
redisCache.setCacheObject(userKey, loginMobileUser, expireTime, TimeUnit.MINUTES);
}
/**
* 设置用户代理信息
*
* @param loginUser 登录信息
*/
public void setUserAgent(LoginUser loginUser)
{
UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
String ip = IpUtils.getIpAddr();
loginUser.setIpaddr(ip);
loginUser.setLoginLocation(AddressUtils.getRealAddressByIP(ip));
loginUser.setBrowser(userAgent.getBrowser().getName());
loginUser.setOs(userAgent.getOperatingSystem().getName());
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private String createToken(Map<String, Object> claims)
{
String token = Jwts.builder()
.setClaims(claims)
.signWith(SignatureAlgorithm.HS512, secret).compact();
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims parseToken(String token)
{
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token)
{
Claims claims = parseToken(token);
return claims.getSubject();
}
/**
* 获取请求token
*
* @param request
* @return token
*/
private String getToken(HttpServletRequest request)
{
String token = request.getHeader(header);
if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX))
{
token = token.replace(Constants.TOKEN_PREFIX, "");
}
return token;
}
/**
* 获取移动端 token ,移动端token,不携带前缀
*
* @param request
* @return token
*/
private String getMobileToken(HttpServletRequest request) {
return request.getHeader(header);
}
private String getTokenKey(String uuid)
{
return CacheConstants.LOGIN_TOKEN_KEY + uuid;
}
/**
* 获取移动端tokenKey
*
* @param uuid
* @return
*/
private String getTokenMobileKey(String uuid) {
return CacheConstants.LOGIN_MOBILE_TOKEN_KEY + uuid;
}
}
7. 定义登录接口
/**
* 移动端用户登录验证
*
* @param username 账号
* @param password 密码
* @param code 验证码
* @param uuid uuid
* @return
*/
public String login(String username, String password, String code, String uuid) {
// 验证码校验
validateCaptcha(username, code, uuid);
// 登录前置校验
loginPreCheck(username, password);
Authentication authenticate;
try {
// 去到 mobileUserDetailsServiceImpl 调用 loadUserByUsername 方法 TODO 登录错误
authenticate = mobileUserAuthenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
throw new UserPasswordNotMatchException();
} else {
throw new ServiceException("登录失败:" + e.getMessage());
}
}
LoginMobileUser curUser = (LoginMobileUser) authenticate.getPrincipal();
return tokenService.createTokenMobile(curUser);
}
8. 修改过滤器
/**
* token过滤器 验证token有效性
*
* @author ruoyi
*/
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
@Autowired
private TokenService tokenService;
// 令牌自定义标识
@Value("${token.header}")
private String header;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
// TODO 分别 pc 端 和 移动端 进行拦截认证
String token = request.getHeader(header);
// 判断token 是否以 Bearer 开头 ,如果是就是 PC 端 否则就是移动端
if (StringUtils.isNotEmpty(token) && token.startsWith("Bearer")) {
// PC 端
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenService.verifyToken(loginUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
} else if (StringUtils.isNotEmpty(token)) {
// 移动端
LoginMobileUser loginMobileUser = tokenService.getLoginMobileUser(request);
if (StringUtils.isNotNull(loginMobileUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
tokenService.verifyTokenMobile(loginMobileUser);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginMobileUser, null, loginMobileUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
} else {
// 没有token 直接放行
chain.doFilter(request, response);
}
}
}
License:
CC BY 4.0