Spring Security实现单体架构下客户端管理端两种登录功能实现-Spring专区论坛-技术-SpringForAll社区

Spring Security实现单体架构下客户端管理端两种登录功能实现

解决目标

在单体架构下,同时容下客户端和管理端,通俗点就是一个项目单服务,同时实现页面A(用户的登录)与页面B(管理员登录),跳转到对应不同的操作端。当然两种身份的用户他不能随意进入其他端,所以需要两种不同的令牌生成和验证。用户表拆分为两个:一个normal_user表,一个super_user表,而不是简简单一个表加权限字段。

初期我是搭2个项目,但是存在部分交互,比如用户审核,商品审核等等功能。然后管理端数据库,用户端数据库,共享数据库3种,感觉这样分开系统,数据可以更好的后期拓展。但是没啥设计经验,设计的不太好,而且在交互上采用传统http调用,感觉这种调用感觉不行,感觉没法处理抛出来的异常,后续暂时没有寻找合适的解决方案(RPC?),感觉微服务openfegin不错,但是微服务比较吃服务器性能,最终就搞个单体的。感觉有更好的方式,麻烦大佬们留言,这种不同单体服务之间通信,采用何种技术合适。最好能提供链接我学学,手动狗头。

思路解析

此项目只是个demo,文末贴Gitee地址,供大家参考,代码比较粗糙,采用 JDK1.8,使用SpringBoot + SpringSecurity,数据库用mysql 和 缓存Redis,都是老面孔了。如果熟悉SpringSecurity的认证流程,可以直接跳过这个,这个单纯算自我提醒的笔记怕以后忘了

首先,了解下SpringSecurity,认证流程如下:

d2b5ca33bd20231115014700

根据这个流程,并且结合源码了解,我们在默认调用登录时,会注入这个AuthenticationManager的authenticate的方法,其中轮询所有AuthticationProvider并调用supproets()方法判断是否支持此Authentication类,合适后调用Authenticate()方法进行认证。演示源码采用AbstractUserDetailsAuthenticationProvider这个类的

d2b5ca33bd20231115014730

根据他源码我们得出结论1:我写入多个继承于 AuthenticationProvider 类的子类,它可以自动选择对应的子类进行登录逻辑处理,而不需要人为的指定那个方式。 具体查看源码:

d2b5ca33bd20231115014753

从中得知,他是根据AuthenticationToken的类型进行判断,那么基于先前的结论1,得出同时写子类并继承 AbstractAuthenticationToken 与 AuthentiocationProvider即可进行自由选择不同的登录方式 如普通用户登录,需要写NormalAuthenticationToken 与 NormalAuthenticationProvider两个类,会进入普通用户登录逻辑处理。而管理员则对应写其他两个,进入管理员登录逻辑。在登录时或者拦截器处理时候则需要生成对应的AuthenticationToken即可,这个在后面代码具体实现查看。

我们继续往下看,返回AbstractUserDetailsAuthenticationProvider中,其中认证方法中,两个个方法:一个是retrieveUser(),以及additionalAuthenticationChecks()。 前者就是调用UserDetailService从数据库中获取用户数据,后者对加密数据进行处理。

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
         () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
               "Only UsernamePasswordAuthenticationToken is supported"));
   String username = determineUsername(authentication);
   boolean cacheWasUsed = true;
   UserDetails user = this.userCache.getUserFromCache(username);
   if (user == null) {
      cacheWasUsed = false;
      try {
         user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      }
      catch (UsernameNotFoundException ex) {
         this.logger.debug("Failed to find user '" + username + "'");
         if (!this.hideUserNotFoundExceptions) {
            throw ex;
         }
         throw new BadCredentialsException(this.messages
               .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
      }
      Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
   }
   try {
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
   }
   catch (AuthenticationException ex) {
      if (!cacheWasUsed) {
         throw ex;
      }
      // There was a problem, so try again after checking
      // we're using latest data (i.e. not from the cache)
      cacheWasUsed = false;
      user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
      this.preAuthenticationChecks.check(user);
      additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
   }
   this.postAuthenticationChecks.check(user);
   if (!cacheWasUsed) {
      this.userCache.putUserInCache(user);
   }
   Object principalToReturn = user;
   if (this.forcePrincipalAsString) {
      principalToReturn = user.getUsername();
   }
   return createSuccessAuthentication(principalToReturn, authentication, user);
}

retriverUser代码:

d2b5ca33bd20231115014838

 

additionalAuthenticationChecks代码:

d2b5ca33bd20231115014843

 

对源码看了下认证流程,得出重写Provider,Token以及userDetailService 即可实现多种方式登录,然后再加上一个拦截器可以做到用户登录,访问分离。

具体代码

首先两个UserDetailsService逻辑:

@Component
public class AdminDetailsService implements UserDetailsService {

    @Autowired
    private SuperUserService superUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        System.err.println("进入了管理员验证功能!!-------------------------------------");
        // 通过用户名从数据库获取用户信息
        SuperUser userDTO = superUserService.getOne(new QueryWrapper<SuperUser>().eq("login_name",username));

        // 用户不存在
        if (userDTO == null) {
            throw new AccountExpiredException(ErrorConstants.LOGIN_ERROR_NOTFOUND);
        }

        // 权限集合
        List<GrantedAuthority> authorities = new ArrayList<>();

        return new User(
                userDTO.getLoginName (),
                userDTO.getPassword(),
                authorities
        );
    }
}
@Component
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private NormalUserService normalUserService;

    @Override
    public User loadUserByUsername(String username) throws UsernameNotFoundException {
        System.err.println("进入了普通验证功能!!-------------------------------------");
        // 通过用户名从数据库获取用户信息
        NormalUser userDTO = normalUserService.getOne(new QueryWrapper<NormalUser>().eq("login_name",username));

        // 用户不存在
        if (userDTO == null) {
            throw new AccountExpiredException (ErrorConstants.LOGIN_ERROR_NOTFOUND);
        }



        // 权限集合
        List <GrantedAuthority> authorities = new ArrayList <> ();

        return new User (
                userDTO.getLoginName (),
                userDTO.getPassword(),
                authorities
        );
    }
}

其次是对应的authenticationToken 这个无脑CV原有的Token 改个名字就好了

public class NormalAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    private Object credentials;

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     *
     */
    public NormalAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     * @param principal
     * @param credentials
     * @param authorities
     */
    public NormalAuthenticationToken(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;
    }

}
public class SuperAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;

    private Object credentials;

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     *
     */
    public SuperAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     * @param principal
     * @param credentials
     * @param authorities
     */
    public SuperAuthenticationToken(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;
    }
}

接着是对应的TokenProvider

@Component
@Slf4j
public class NormalAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private CustomUserDetailsService userDetailsService;

    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        String password =(String) authentication.getCredentials();
        try {
            UserDetails loadedUser = userDetailsService.loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            Collection<? extends GrantedAuthority> authorities = loadedUser.getAuthorities();
            if (!passwordEncoder.matches(password, loadedUser.getPassword())) {
                throw new UsernameNotFoundException("账号密码错误");
            }
            return new NormalAuthenticationToken(loadedUser,password,authorities);
        }
        catch (UsernameNotFoundException ex) {
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
        
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (NormalAuthenticationToken.class).isAssignableFrom(authentication);
    }
    
}
@Component
@Slf4j
public class SuperAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private AdminDetailsService service;

    @Autowired
    private PasswordEncoder passwordEncoder;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        String password =(String) authentication.getCredentials();
        try {
            UserDetails loadedUser = service.loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            Collection<? extends GrantedAuthority> authorities = loadedUser.getAuthorities();
            if (!passwordEncoder.matches(password, loadedUser.getPassword())) {
                throw new UsernameNotFoundException("账号密码错误");
            }
            return new SuperAuthenticationToken(loadedUser,password,authorities);
        }
        catch (UsernameNotFoundException ex) {
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }

    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (SuperAuthenticationToken.class).isAssignableFrom(authentication);
    }
}

以上是基础的类,我这边两种登陆方式,所以只需要两个,如果多种需要多个对应的创建。

以下是SpringSecuirty的配置,正常配置,主要是配置AuthenticationManager的时候稍有区别

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法级安全验证
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private  UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
    @Autowired
    private JwtAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private JwtFilter jwtFilter;

    @Autowired
    private NormalAuthenticationProvider normalAuthenticationProvider;

    @Autowired
    private SuperAuthenticationProvider superAuthenticationProvider;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //不进行权限验证的请求或资源(从配置文件中读取)
                .antMatchers("/login").permitAll()
                .antMatchers("/adminLogin").permitAll()
                //其他的需要登陆后才能访问
                .anyRequest().authenticated()
                .and()
                //配置未登录自定义处理类
                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                //配置登录地址
                .formLogin()
                .loginProcessingUrl("/login/userLogin")
                .and()
                //配置没有权限自定义处理类
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
                .and()
                // 开启跨域
                .cors()
                .and()
                // 取消跨站请求伪造防护
                .csrf().disable();
        // 基于Token不需要session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        // 禁用缓存
        http.headers().cacheControl();
        // 添加JWT过滤器
        http.addFilterBefore(jwtFilter,UsernamePasswordAuthenticationFilter.class);
    }

    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        AuthenticationManager auth = new ProviderManager(Arrays.asList(superAuthenticationProvider,normalAuthenticationProvider));
        return auth;
//        return super.authenticationManager();
    }
}

其次是拦截器的实现,采用正则表达式拦截请求API并且进行认证,我这边处理验证是否有token,其他直接甩给SpringSecurity的过滤器去做处理也就是AuthenticationEntryPoint类处理,导致有个问题:用户无论是登错系统,或者密码账号错误都会报同一个错误类型,所以我的登陆错系统再过滤器处理了。

@Component
public class JwtFilter extends OncePerRequestFilter {
    @Autowired
    JwtTokenUtil jwtTokenUtil;

    private static Pattern patternNormal = Pattern.compile("^/normal/.*$");

    private static Pattern patternAdmin = Pattern.compile("^/admin/.*$");
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader("token");
        // 没有token,去走登录流程
        if (StringUtils.isBlank(token)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            return;
        }
        String servletPath = httpServletRequest.getServletPath();
        try {
            if (patternNormal.matcher(servletPath).matches()) {
                if (JwtTokenUtil.validateToken(token)) {
                    Authentication authentication = JwtTokenUtil.getNormalAuthentication(token);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
                }
            } else if (patternAdmin.matcher(servletPath).matches()) {
                if (JwtTokenUtil.validateAdminToken(token)) {
                    Authentication authentication = JwtTokenUtil.getAdminAuthentication(token);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    filterChain.doFilter(httpServletRequest, httpServletResponse);
                }

            }
        }catch (SignatureVerificationException e){
            String errMsg = ErrorConstants.LOGIN_ERROR_SYSTEM;
            httpServletResponse.setStatus( HttpStatus.UNAUTHORIZED.value () );
            httpServletResponse.setCharacterEncoding("UTF-8");
            httpServletResponse.setContentType("application/json; charset=utf-8");
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.write(JSON.toJSONString(Result.error(errMsg)));
            printWriter.flush();
        }

    }

    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("^/normal/.*$");
        String a ="/normal/visit";
        Matcher matcher = pattern.matcher(a);
        System.out.println(matcher.matches());
    }
}
/**
 * 用户登录失败处理
 */
@Slf4j
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        String errMsg;
        String token = request.getHeader("token");
        if( StringUtils.isEmpty ( token ) ){ // 没有token抛出的异常

            errMsg = StringUtils.isEmpty(authException.getMessage()) ? ErrorConstants.LOGIN_ERROR_NOT_LOGIN_IN : authException.getMessage();
            response.setStatus( HttpStatus.UNAUTHORIZED.value () );
        }

        else {
            errMsg = ErrorConstants.LOGIN_ERROR_TIMEOUT;
            response.setStatus( HttpStatus.REQUEST_TIMEOUT.value () );
        }
        log.error (errMsg);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        PrintWriter printWriter = response.getWriter();
        printWriter.write(JSON.toJSONString(Result.error(errMsg)));
        printWriter.flush();
    }
}

作者:用户4108570294675
链接:https://juejin.cn/post/7292984695992959003

请登录后发表评论

    没有回复内容