若依多用户表登录(sping security多用户)
若依分离版新增用户表怎么实现和后台管理互不影响的登录?这篇文章教你怎么实现登录时分表查询!
目录
声明自定义AuthenticationManager 的bean
我们定义了UserDetailsService类型,和若依的冲突了
我们定义的AuthenticationManager与若依的冲突了
写在前面
场景:
用若依搭建的后台管理环境,但是前台用户系统(前端)并没有和若依的前端集成在一起。是两个独立的前端项目,但是后端是集成在一起的。
我现在有一个会员表,若依有个系统用户表,如果想要会员登录(前台用户系统)和后台登录(后台管理系统)互不干扰怎么实现(两个登录接口)?
。。。
若依分离版使用的是sping security框架,所以本质上是sping security如何实现多用户!如果你已经具备springsecurity的知识,那么你会更轻松!
本文分为两个方法,
方法一:利用spring security框架帮我们调用查询用户表,登录流程与若依一致,比较复杂,
方法二:手动查询用户表,利用若依生成token,简单粗暴
建议配合目录阅读
方法一(可能复杂)
明确步骤
准备:准备自定义的用户实体类,强烈建议放在common模块中!
- 登录时查询我们自己的表,缓存用户信息时缓存我们用户实体类型:
- 实现UserDetailsService接口;
- 改造loginuser类;
- 自己声明AuthenticationManager 的bean;
- 自定义登录接口:
- 使用我们自定义的AuthenticationManager
- 从token能够获取登录人的信息
- 前端携带token
经过分析,我们已经知道实现这个的关键和步骤,下面正片开始
前期准备
准备一个我们自己的用户表实体类,假设叫做ShopUser,这个实体类再多个模块中使用,强烈建议放在common模块中!
实现UserDetailsService接口
熟悉springsecurity的都知道,登录时查询用户表是依靠UserDetailsService接口实现的,我们想要登录时查询我们自己的会员表的话,就需要实现这个接口,参照若依的UserDetailsServiceImpl,我们假定他叫MemberDetailsServiceImpl(请定义在framework模块!建议参考若依的userSetailServiceImpl的位置)
@Component("MemberDetailsServiceImpl")
public class MemberDetailsServiceImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ShopUserMapper memberMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
ShopUser member = memberMapper.selectShopUserByPhone(username);//验证登录用户,查询数据库,如果这个mapper定义在自己的模块,引入maven依赖不用我多说吧?
System.out.println("这里是会员登录"+member);
if (StringUtils.isNull(member)) {
log.info("登录用户:{} 不存在.", username);
throw new ServiceException("登录用户:" + username + " 不存在");
}
return createLoginUser(member);
}
public UserDetails createLoginUser(ShopUser member) {
return new LoginUser(member.getId(), member);
}
}
这里直接这样操作,你应该是会报错的,
因为若依的LoginUser有参构造并没有这个类型的参数!这个问题先不急,我们有两条路可走
方案一:
在若依的LoginUser新增我们的自定义用户类型,并提供对应的getter setter方法,提供初始化的构造方法
方案二:
自定义一个LoginUser,取名什么的都无所谓(但是不要重名哈哈),只要记得实现UserDetails接口就行(springsecurity的内容),里面可以自定义一些被缓存的属性
。。。。
这两个方案都是可行的,怎么采用是自己的选择,当然你从上面的代码都能知道我是直接采用的方法一(因为简单,哈哈),如果你选择方案二,也是可以自己实现的,没什么区别
接着上面方案一,我们只需要小小的修改若依的LoginUser就ok了
改造loginUser
我们需要找到这个LongUser
然后新增一个属性,是我们自己的会员类型
提供对应的构造函数,由于我的会员id也是Long类型,我就直接使用若依LoginUser原本的userId了,并且我没有设置权限角色相关信息,所以构造函数里面只有这两个,如果要添加权限之类的,那就自己添加形参参数了
为了springsecurity获取用户信息,我们需要将获取用户名和密码的getter方法小小的修改一下,如果user是空的就返回我们shopUser的用户名和密码,你看上面的@Override就知道这是UserDetails的方法,只不过需要我们重写他
哦,对了shopUser(我们自定义的用户类型),记得也要定义一个get还有set方法,不然等你要获取用户信息的时候有你哭的,哈哈。
注:我看评论区里面有伙伴反应获取用户空指针的问题,是因为忘记写set方法。我只能说,要细心!(犯低级错误实在不应该,可能是因为我红圈没有圈到的原因哈哈)
我还是粘贴一下代码吧,方便观察
/**
* 登录用户身份权限
*
* @author ruoyi
*/
public class LoginUser implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 部门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 ShopUser shopUser;
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, ShopUser shopUser) {
this.userId = userId;
this.shopUser = shopUser;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
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 shopUser.getPassword();
}
}
@Override
public String getUsername() {
if (user != null) {
return user.getUserName();
} else return shopUser.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 ShopUser getShopUser() {
return shopUser;
}
public void setShopUser(ShopUser shopUser) {
this.shopUser = shopUser;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
你以为到这第一步就完成了吗?不,没有,
到这我们自定义了一个MemberDetailsServiceImpl,修改了LongUser,但是如果我们直接抄若依的登录,他不会生效!
声明自定义AuthenticationManager 的bean
为什么不会生效呢?其中关键的是,就像他注释下的那样,其中关键
是这个authenticationManager调用的是若依定义的UserDetailsServiceImpl而不是我们定义的MemberDetailsServiceImpl,那我们怎么样才能让他调用我们定义的MemberDetailsServiceImpl呢?其中关键的是authenticationManager,他决定了调用哪一个
一看@Resource就知道,他注入了一个bean!那他肯定声明了这个bean!我们要找到这个AuthenticationManager的bean声明
这个AuthenticationManager用的就是若依定义的userDetailsService!你翻到下面最后一行就知道了
那我们怎么实现使用我们定义的MemberDetailsServiceImpl呢?好吧,你已经看到上面的代码了,我就不卖关子了。。。
没错!我们直接复制一份,稍微修改一下就行了,指定我们需要使用的是memberDetailsService
!
记得之前我们定义的MemberDetailsServiceImpl要注入哦,这里有涉及到spring依赖注入的问题,怎么解决我这里不细说(文章最后有讲解)。
代码
/**
* spring security配置
*
* @author ruoyi
*/
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
/**
* 自定义用户认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
@Autowired
@Qualifier("MemberDetailsServiceImpl")
private UserDetailsService memberDetailsService;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPointImpl unauthorizedHandler;
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
/**
* 跨域过滤器
*/
@Autowired
private CorsFilter corsFilter;
/**
* 解决 无法直接注入 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(memberDetailsService);
authenticationProvider.setPasswordEncoder(NoOpPasswordEncoder.getInstance());//明文密码存储
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
{
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/login", "/register", "/captchaImage").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/profile/**"
).permitAll()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
.antMatchers("/druid/**").anonymous()
//开放全局访问接口(包括带token访问)
.antMatchers("/goods/goods/**").permitAll()
.antMatchers("/goods/**/**").permitAll()
.antMatchers("/member/member/**").permitAll()
.antMatchers("/order/order").permitAll()
.antMatchers("/order/**/**").permitAll()
.antMatchers("/userCollection/**").permitAll()
.antMatchers("/commonfile/**").permitAll()
.antMatchers("/itemize/**").permitAll()
.antMatchers("/specs/**/**").permitAll()
.antMatchers("/specs/**/**").permitAll()
.antMatchers("/goodx/**").permitAll()
.antMatchers("/money/**").permitAll()
.antMatchers("/add/money/**").permitAll()
.antMatchers("/shopfootmark/**").permitAll()
.antMatchers("/index/**").permitAll()
.antMatchers("/address/**").permitAll()
.antMatchers("/Shopping/api/v1/order/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.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);
}
/**
* 强散列哈希加密实现
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder()
{
return new BCryptPasswordEncoder();
}
/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
自定义登录接口
上面我们自定义了一个AuthenticationManager,并且这个AuthenticationManager指向的是我们自己的MemberDetailsService。那我们就需要在使用这个AuthenticationManager。
注:若依的登录逻辑在哪个模块,我们就在哪个模块定义最好,我这里就懒得写了,直接定义在framework模块中了
代码
@RestController
public class MemberLoginService {
@Resource
@Qualifier("MemberAuthenticationManager")
private AuthenticationManager authenticationManager;
@Autowired
private TokenService tokenService;
/**
* 会员登录验证
*
* @param shopUser
* @return {@link String}
*/
@PostMapping("/member/member/login")
public AjaxResult login(@RequestBody ShopUser shopUser) {
// 用户验证
Authentication authentication;
try {
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(shopUser.getPhone(), shopUser.getPassword()));
} catch (Exception e) {
if (e instanceof BadCredentialsException) {
throw new UserPasswordNotMatchException();
} else {
throw new ServiceException(e.getMessage());
}
}
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String token = tokenService.createToken(loginUser);
// 生成token
HashMap<String, Object> map = new HashMap<>();
map.put("token",token);
map.put("user",loginUser.getShopUser());
return AjaxResult.success("登录成功",map);
}
}
从token能够获取登录人的信息
baseConroller是若依定义的一个基类,封装了一下常用的方法,这里就不细说了
大功告成!
测试,前端携带token
我们查看若依的后台前端,知道怎么携带token
我们直接抄就行了!在登录成功后缓存,在请求拦截器里面添加就行了。这里不细说了。。。
bean重复的问题
上面说过,由于注入相同类型的bean,以及声明多个相同的bean,会出现bean重复的问题。
对于多个同类型bean的解决方法(三步骤):
- bean定义的时候,设置bean名称
- 使用@Primary注解,标注一个bean为主要的bean(如果注入的时候不指定bean名称,优先注入这个被标记的bean)
- 注入的时候,指定注入的bean名称
好了,三步骤我们已经知道,我们只有两个地方涉及到同类型bean
我们定义了UserDetailsService类型,和若依的冲突了
定义的时候指定bean名称,
在若依的UserDetailsServiceImpl添加@Primary注解,表示这个是主要的bean
注入的时候指定bean名称
我们定义的AuthenticationManager与若依的冲突了
依旧是三步
注入我们自定义的时候指定名称
方法一介绍到这里
方法二(简单粗暴)
如果你感觉只是想要能够登录,能够获取token信息,能够获取到token对应的用户信息而已,有必要那么复杂吗?那么方法二可能适合你!
阅读若依的源码,可以得出登录最核心的一个步骤就是
其他部分都是验证用户以及日志记录,只有这个生成token这里才是将令牌进行生成以及存储。所以咱就只要这一点就可以了。
如果你想要在LoginUser里面存一些其他的信息,比如:手机号,性别,昵称等等,可以自己修改LoginUser。怎么修改,可以看方法一中的【改造LoginUser】这一步
完事!是不是很简单?
更多推荐
所有评论(0)