目录
效果如下
需求背景
用若依搭建的后台管理环境,但是前台用户系统(前端)并没有和若依的前端集成在一起。是两个独立的前端项目,但是后端是集成在一起的。
我现在有一个前端会员表,若依有个系统用户表,如果想要会员登录(前台用户系统)和后台登录(后台管理系统)互不干扰怎么实现(两个登录接口)?
若依分离版使用的是sping security框架,所以本质上是sping security如何实现多用户!如果你已经具备springsecurity的知识,那么你会更轻松!
明确步骤
准备:准备自定义的用户实体类,强烈建议放在common中!
1、登录时查询我们自己的表,缓存用户信息时缓存我们用户实体类型:
实现UserDetailsService接口;
改造loginuser类;
自己声明AuthenticationManager 的bean;
2、自定义登录接口:
使用我们自定义的AuthenticationManager
3、从token能够获取登录人的信息
4、前端携带token
前期准备
准备一个我们自己的用户表实体类,假设叫做CustomerUser,这个实体类再多个模块中使用,强烈建议放在common模块中!
实现UserDetailsService接口
熟悉springsecurity的都知道,登录时查询用户表是依靠UserDetailsService接口实现的,我们想要登录时查询我们自己的会员表的话,就需要实现这个接口,参照若依的UserDetailsServiceImpl,我们假定他叫CustomerDetailsServiceImpl(请定义在framework模块!建议参考若依的userSetailServiceImpl的位置)
代码如下
package com.ruoyi.framework.web.service;
import com.ruoyi.common.core.domain.entity.CustomerUser;
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.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.customer.mapper.CustomerUserMapper;
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.Component;
/**
* 用户验证处理
*
* @author ruoyi
*/
@Component("CustomerDetailsServiceImpl")
public class CustomerDetailsServiceImpl implements UserDetailsService
{
private static final Logger log = LoggerFactory.getLogger(CustomerDetailsServiceImpl.class);
@Autowired
private CustomerUserMapper customerUserMapper;
@Autowired
private CustomerPasswordService customerPasswordService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
CustomerUser user = customerUserMapper.selectUserByUserName(username);
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException(MessageUtils.message("user.not.exists"));
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException(MessageUtils.message("user.password.delete"));
}
else if (UserStatus.DISABLE.getCode().equals(user.getActive()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
customerPasswordService.validate(user);
return createLoginUser(user);
}
public UserDetails createLoginUser(CustomerUser user)
{
return new LoginUser(user.getUserId(), user);
}
}
改造loginUser
新增一个属性,CustomerUser
代码如下
package com.ruoyi.common.core.domain.model;
import com.alibaba.fastjson2.annotation.JSONField;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.entity.CustomerUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Set;
/**
* 登录用户身份权限
*
* @author ruoyi
*/
public class LoginUser implements UserDetails
{
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 邀请用户ID
*/
private Long inviteId;
/**
* 部门ID
*/
private Long deptId;
/**
* 用户唯一标识
*/
private String token;
/**
* 登录时间
*/
private Long loginTime;
/**
* 过期时间
*/
private Long expireTime;
/**
* 登录IP地址
*/
private String ipaddr;
/**
* 登录地点
*/
private String loginLocation;
/**
* 浏览器类型
*/
private String browser;
/**
* 操作系统
*/
private String os;
/**
* 权限列表
*/
private Set<String> permissions;
/**
* 用户信息
*/
private SysUser user;
/**
* 客户信息
*/
private CustomerUser customerUser;
public LoginUser()
{
}
public LoginUser(SysUser user, Set<String> permissions)
{
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions)
{
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
}
public LoginUser(Long userId, CustomerUser user)
{
this.userId = userId;
this.customerUser = user;
}
public Long getUserId() { return userId; }
public void setUserId(Long userId)
{
this.userId = userId;
}
public Long getInviteId() { return inviteId; }
public void setInviteId(Long inviteId)
{
this.inviteId = inviteId;
}
public Long getDeptId()
{
return deptId;
}
public void setDeptId(Long deptId)
{
this.deptId = deptId;
}
public String getToken()
{
return token;
}
public void setToken(String token)
{
this.token = token;
}
@JSONField(serialize = false)
@Override
public String getPassword()
{
if(user != null){
return user.getPassword();
}else{
return customerUser.getPassword();
}
}
@Override
public String getUsername()
{
if(user != null){
return user.getUserName();
}else{
return customerUser.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;
}
public Long getLoginTime()
{
return loginTime;
}
public void setLoginTime(Long loginTime)
{
this.loginTime = loginTime;
}
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;
}
public Long getExpireTime()
{
return expireTime;
}
public void setExpireTime(Long expireTime)
{
this.expireTime = expireTime;
}
public Set<String> getPermissions()
{
return permissions;
}
public void setPermissions(Set<String> permissions)
{
this.permissions = permissions;
}
public SysUser getUser(){ return user; }
public void setUser(SysUser user)
{
this.user = user;
}
public CustomerUser getCustomerUser(){ return customerUser; }
public void setCustomerUser(CustomerUser customerUser) { this.customerUser = customerUser; }
@Override
public Collection<? extends GrantedAuthority> getAuthorities()
{
return null;
}
}
声明自定义AuthenticationManager 的bean
原因
是这个authenticationManager调用的是若依定义的UserDetailsServiceImpl而不是我们定义的CustomerDetailsServiceImpl ,那我们怎么样才能让他调用我们定义的CustomerDetailsServiceImpl 呢?其中关键的是authenticationManager,他决定了调用哪一个
一看@Resource就知道,他注入了一个bean!那他肯定声明了这个bean!我们要找到这个AuthenticationManager的bean声明
这个AuthenticationManager用的就是若依定义的userDetailsService!
package com.ruoyi.framework.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.ruoyi.framework.config.properties.PermitAllUrlProperties;
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
/**
* spring security配置
*
* @author ruoyi
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 自定义用户认证逻辑
*/
@Autowired
@Qualifier("CustomerDetailsServiceImpl")
private UserDetailsService customerDetailsServiceImpl;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 允许匿名访问的地址
*/
@Autowired
private PermitAllUrlProperties permitAllUrl;
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
@Primary
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Bean("MemberAuthenticationManager")
public AuthenticationManager MemberAuthenticationManagerBean() throws Exception
{
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customerDetailsServiceImpl);
// BCryptPasswordEncoder 使用BCrypt的强散列哈希加密实现,并可以由客户端指定加密的强度strength,强度越高安全性自然就越高,默认为10.
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// authenticationProvider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());//明文密码存储
authenticationProvider.setPasswordEncoder(passwordEncoder);// 加密密码存储
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 | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
// 注解标记允许匿名访问的url
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
// anonymous() 允许匿名用户访问,不允许已登入用户访问
// permitAll() 不管登入,不登入 都能访问
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 禁用HTTP响应标头
.headers().cacheControl().disable().and()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").permitAll()
.antMatchers( "/app/employee/user/employeeUserList").permitAll()
.antMatchers( "/employee/user/employeeUserList", "/employee/user/info/*").permitAll()
.antMatchers( "/system/dict/data/list").permitAll()
.antMatchers("/customer/info/customerLogin").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
// 添加Logout filter
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);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
自定义登录接口
上面我们自定义了一个AuthenticationManager,并且这个AuthenticationManager指向的是我们自己的CustomerDetailsService。那我们就需要在使用这个AuthenticationManager。
package com.ruoyi.web.controller.module.customer;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.CustomerUser;
import com.ruoyi.common.core.domain.model.LoginBody;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.CustomerLoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* 登录验证
*
* @author ruoyi
*/
@RestController
@RequestMapping("/customer/info")
public class CustomerInfoController
{
@Autowired
private CustomerLoginService customerLoginService;
/**
* 登录方法
*
* @param loginBody 登录信息
* @return 结果
*/
@PostMapping("/customerLogin")
public AjaxResult customerLogin(@RequestBody LoginBody loginBody)
{
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = customerLoginService.customerLogin(loginBody.getUsername(), loginBody.getPassword());
ajax.put(Constants.TOKEN, token);
return ajax;
}
/**
* 获取用户信息
*
* @return 用户信息
*/
@GetMapping("getCustomerInfo")
public AjaxResult getCustomerInfo()
{
CustomerUser user = SecurityUtils.getLoginUser().getCustomerUser();
AjaxResult ajax = AjaxResult.success();
ajax.put("user", user);
return ajax;
}
}
sevice代码
package com.ruoyi.framework.web.service;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.exception.user.*;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.common.core.domain.entity.CustomerUser;
import com.ruoyi.customer.service.ICustomerUserService;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.context.AuthenticationContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 登录校验方法
*
* @author ruoyi
*/
@Component
public class CustomerLoginService
{
// 是否允许账户多终端同时登录(true允许 false不允许)
@Value("${token.soloLogin}")
private boolean soloLogin;
@Autowired
private RedisCache redisCache;
@Resource
@Qualifier("MemberAuthenticationManager")
private AuthenticationManager authenticationManager;
@Autowired
private ICustomerUserService customerUerService;
@Autowired
private TokenService tokenService;
/**
* 登录验证
*
* @param username 用户名
* @param password 密码
* @return 结果
*/
public String customerLogin(String username, String password)
{
// 登录前置校验
loginPreCheck(username, password);
CustomerUser user = null;
// 用户验证
Authentication authentication = null;
try
{
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
System.out.println("authenticationToken");
System.out.println(authenticationToken);
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用CustomerDetailsServiceImpl.loadUserByUsernam
authentication = authenticationManager.authenticate(authenticationToken);
System.out.println("authentication");
System.out.println(authentication);
}
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());
if (!soloLogin)
{
// 如果用户不允许多终端同时登录,清除缓存信息
String userIdKey = Constants.LOGIN_USERID_KEY + loginUser.getCustomerUser().getUserId();
String userKey = redisCache.getCacheObject(userIdKey);
if (StringUtils.isNotEmpty(userKey))
{
redisCache.deleteObject(userIdKey);
redisCache.deleteObject(userKey);
}
}
// 生成token
return tokenService.createToken(loginUser);
}
/**
* 登录前置校验
* @param username 用户名
* @param password 用户密码
*/
public void loginPreCheck(String username, String password)
{
// 用户名或密码为空 错误
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
throw new UserNotExistsException();
}
// 密码如果不在指定范围内 错误
if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
|| password.length() > UserConstants.PASSWORD_MAX_LENGTH)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
// 用户名不在指定范围内 错误
if (username.length() < UserConstants.USERNAME_MIN_LENGTH
|| username.length() > UserConstants.USERNAME_MAX_LENGTH)
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
throw new UserPasswordNotMatchException();
}
}
/**
* 记录登录信息
*
* @param userId 用户ID
*/
public void recordLoginInfo(Long userId)
{
CustomerUser customerUser = new CustomerUser();
customerUser.setUserId(userId);
customerUser.setLoginIp(IpUtils.getIpAddr());
customerUser.setLoginDate(DateUtils.getNowDate());
customerUerService.updateUserProfile(customerUser);
}
}
参考文章
若依分离版新增用户表实现会员与后台分别登录(sping security多用户)_若依怎么向sys_user添加数据-CSDN博客