目录

写在前面

方法一(可能复杂)

明确步骤

前期准备

实现UserDetailsService接口

改造loginUser

声明自定义AuthenticationManager 的bean

自定义登录接口

从token能够获取登录人的信息

测试,前端携带token

bean重复的问题

我们定义了UserDetailsService类型,和若依的冲突了

我们定义的AuthenticationManager与若依的冲突了

方法二(简单粗暴)


写在前面

场景:

用若依搭建的后台管理环境,但是前台用户系统(前端)并没有和若依的前端集成在一起。是两个独立的前端项目,但是后端是集成在一起的。

我现在有一个会员表,若依有个系统用户表,如果想要会员登录(前台用户系统)和后台登录(后台管理系统)互不干扰怎么实现(两个登录接口)?

。。。

若依分离版使用的是sping security框架,所以本质上是sping security如何实现多用户!如果你已经具备springsecurity的知识,那么你会更轻松!

本文分为两个方法,

方法一:利用spring security框架帮我们调用查询用户表,登录流程与若依一致,比较复杂,

方法二:手动查询用户表,利用若依生成token,简单粗暴

建议配合目录阅读

方法一(可能复杂)

明确步骤

准备:准备自定义的用户实体类,强烈建议放在common模块中!

  1. 登录时查询我们自己的表,缓存用户信息时缓存我们用户实体类型:
    1. 实现UserDetailsService接口;
    2. 改造loginuser类;
    3. 自己声明AuthenticationManager 的bean;
  2. 自定义登录接口:
    1. 使用我们自定义的AuthenticationManager
  3. 从token能够获取登录人的信息
  4. 前端携带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的解决方法(三步骤):

  1. bean定义的时候,设置bean名称
  2. 使用@Primary注解,标注一个bean为主要的bean(如果注入的时候不指定bean名称,优先注入这个被标记的bean)
  3. 注入的时候,指定注入的bean名称

好了,三步骤我们已经知道,我们只有两个地方涉及到同类型bean

我们定义了UserDetailsService类型,和若依的冲突了

定义的时候指定bean名称,

在若依的UserDetailsServiceImpl添加@Primary注解,表示这个是主要的bean

注入的时候指定bean名称

我们定义的AuthenticationManager与若依的冲突了

依旧是三步

注入我们自定义的时候指定名称

方法一介绍到这里

方法二(简单粗暴)

如果你感觉只是想要能够登录,能够获取token信息,能够获取到token对应的用户信息而已,有必要那么复杂吗?那么方法二可能适合你!

阅读若依的源码,可以得出登录最核心的一个步骤就是

其他部分都是验证用户以及日志记录,只有这个生成token这里才是将令牌进行生成以及存储。所以咱就只要这一点就可以了。

如果你想要在LoginUser里面存一些其他的信息,比如:手机号,性别,昵称等等,可以自己修改LoginUser。怎么修改,可以看方法一中的【改造LoginUser】这一步

完事!是不是很简单?

Logo

快速构建 Web 应用程序

更多推荐