Security前后端分离多种认证方式


Security在现阶段的开发中使用频率非常高,用于权限认证;项目前后端分离的认证,前端登录也不仅仅是单一的账号密码登录,常常会有验证码登录以及各种第三方登录等等,下面我们就一一讲解Security的实现方式。

源码地址:戳我查看

1. 认证流程

先列出几个关键文件

  • UsernamePasswordAuthenticationFilter
  • UsernamePasswordAuthenticationToken
  • AuthenticationManager(ProviderManager)
  • AuthenticationProvider
    Security整个认证流程可以理解为对过滤器以及认证管理的CRUD,第一步是添加过滤器,第二部添加令牌对象,第三步添加认证处理器,最后添加到配置中就结束了。

UsernamePasswordAuthenticationFilter

这是Security默认的过滤器,继承AbstractAuthenticationProcessingFilter,看个源码片段

private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
            "POST");

public UsernamePasswordAuthenticationFilter() {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
    super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
    throws AuthenticationException {
    if (this.postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
    }
    String username = obtainUsername(request);
    username = (username != null) ? username : "";
    username = username.trim();
    String password = obtainPassword(request);
    password = (password != null) ? password : "";
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
    // Allow subclasses to set the "details" property
    setDetails(request, authRequest);
    return this.getAuthenticationManager().authenticate(authRequest);
}

这里过滤器做了两件事,添加过滤路径,从request中获取表单传入的用户名及密码生成令牌对象传入认证管理器。

UsernamePasswordAuthenticationToken

没什么好说的看源码就行,接收用户名密码封装对象,这里可以添加自定义数据。

AuthenticationManager

ProviderManager的源码中有这样一段代码:

private List<AuthenticationProvider> providers = Collections.emptyList();

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    AuthenticationException parentException = null;
    Authentication result = null;
    Authentication parentResult = null;
    int currentPosition = 0;
    int size = this.providers.size();
    for (AuthenticationProvider provider : getProviders()) {
        if (!provider.supports(toTest)) {
            continue;
        }
        if (logger.isTraceEnabled()) {
            logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
                                           provider.getClass().getSimpleName(), ++currentPosition, size));
        }
        try {
            result = provider.authenticate(authentication);
            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }
        catch (AccountStatusException | InternalAuthenticationServiceException ex) {
            prepareException(ex, authentication);
            // SEC-546: Avoid polling additional providers if auth failure is due to
            // invalid account status
            throw ex;
        }
        catch (AuthenticationException ex) {
            lastException = ex;
        }
    }
    ...

这里for循环遍历了所有的认证器,调用每个认证器的supports方法来判断是否执行下面的authenticate认证方法。

AuthenticationProvider

这是一个接口,里面只有两个方法:

Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);

authenticate是用于实现认证的,supports用于判断是否执行该认证器的,只有当supports返回true的时候才会执行。

2. 前后端分离认证

前面讲了认证流程,这里我们就走一遍这个流程,单认证器实现前后端分离认证。

2.1 实现过滤器

public class CustomAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private static final String AUTH_TYPE = "authType";
    private static final String USERNAME = "username";
    private static final String PASSWORD = "password";
    private static final String OAUTH_TOKEN_URL = "/home/login";
    private static final String HTTP_METHOD_POST = "POST";

    public CustomAuthenticationProcessingFilter() {
        super(new AntPathRequestMatcher(OAUTH_TOKEN_URL, HTTP_METHOD_POST));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        if (!HTTP_METHOD_POST.equals(request.getMethod().toUpperCase())) {
            throw new AuthenticationServiceException("不支持的请求方式: " + request.getMethod());
        }

        AbstractAuthenticationToken token = new JwtAuthenticatioToken(
                request.getParameter(USERNAME), request.getParameter(PASSWORD), request.getParameter(AUTH_TYPE)
        );
        this.setDetails(request, token);

        return this.getAuthenticationManager().authenticate(token);
    }

    protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

}

2.2 自定义令牌对象

这里前后端都使用这个认证器,在令牌中添加authType字段,便于认证器判断。

public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private String token;

    private String authType;

    public JwtAuthenticatioToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    public JwtAuthenticatioToken(Object principal, Object credentials, String authType) {
        super(principal, credentials);
        this.authType = authType;
    }

    public JwtAuthenticatioToken(Object principal, Object credentials, String authType, String token) {
        super(principal, credentials);
        this.token = token;
        this.authType = authType;
    }

    public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        super(principal, credentials, authorities);
        this.token = token;
    }

    public String getAuthType() {
        return authType;
    }

    public void setAuthType(String authType) {
        this.authType = authType;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

}

2.3 实现认证器

public class AdminAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private PcUserDao pcUserDao;

    @Autowired
    private BackUserDao backUserDao;

    @Override
    public Authentication authenticate(Authentication authentication) {

        if (authentication.getPrincipal() == null) {
            throw new RuntimeException("用户名为空");
        }

        JwtAuthenticatioToken token = (JwtAuthenticatioToken) authentication;
        String authType = token.getAuthType();

        if ("PC".equals(authType)) {
            PcUserEntity byPhone = pcUserDao.findByPhone((String) token.getPrincipal());
            if (byPhone == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
        } else if ("BACK".equals(authType)) {
            BackUserEntity byPhone = backUserDao.findByPhone((String) token.getPrincipal());
            if (byPhone == null) {
                throw new UsernameNotFoundException("用户不存在");
            }

        } else {
            // 其它方式
        }

        return new JwtAuthenticatioToken(authentication.getPrincipal(), authentication.getCredentials(), JwtTokenUtils.generateToken(authentication));
    }

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

}

3. 多种认证方式

上面只是使用了单一的认证器判断自定义字段调用原始的Security账号密码认证逻辑认证;当我们有自己的登录方式时这就满足不了了,下面我们举例说说验证码该如何来进行验证。

只需要对过滤器以及认证器的CRUD即可。

3.1 自定义过滤器

public class MobileAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private static final String USERNAME = "mobile";
    private static final String PASSWORD = "code";
    private static final String OAUTH_TOKEN_URL = "/mobile/login";
    private static final String HTTP_METHOD_POST = "POST";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher(OAUTH_TOKEN_URL,
            HTTP_METHOD_POST);

    private boolean postOnly = true;

    public MobileAuthenticationProcessingFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public MobileAuthenticationProcessingFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String username = request.getParameter(USERNAME);
        username = (username != null) ? username : "";
        username = username.trim();
        String password = request.getParameter(PASSWORD);
        password = (password != null) ? password : "";

        AbstractAuthenticationToken authRequest = new MobileAuthenticationToken(username, password);
        this.setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected void setDetails(HttpServletRequest request, AbstractAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }
}

3.2 自定义令牌

public class MobileAuthenticationToken extends UsernamePasswordAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private String token;

    public MobileAuthenticationToken(Object principal, Object credentials) {
        super(principal, credentials);
    }

    /**
     * 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 MobileAuthenticationToken(Object principal, Object credentials,
                                     Collection<? extends GrantedAuthority> authorities) {
        // 不用返回密码
        super(principal, credentials, authorities);
    }

    public MobileAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
        super(principal, credentials, authorities);
        this.token = token;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

3.3 验证码登录认证器

这里我们可以注入自定义的service,Security默认的UserDetailsService中有一个loadUserByUsername方法用于判断用户状态等。

@Component
public class MobileAuthenticationProvider implements AuthenticationProvider {

    /**
     * 注入自定义的service
     */
    @Autowired
    private MobileUserDetailService mobileUserDetailService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();

        JwtUserDetails mobileDetails = (JwtUserDetails) mobileUserDetailService.loadUserByUsername(username);

        String cachePwd = mobileDetails.getPassword();

        // 比较表单输入的验证码
        if (!cachePwd.equals(password)) {
            throw new BadCredentialsException("验证码错误");
        }

        /*try {
            // 登录认证等
        } catch (Exception e) {
            e.printStackTrace();
            throw new BadCredentialsException(e.getMessage());
        }*/

        MobileAuthenticationToken token = new MobileAuthenticationToken(username, password, mobileDetails.getAuthorities(), JwtTokenUtils.generateToken(authentication));
        token.setToken(JwtTokenUtils.generateToken(authentication));

        return token;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        /**
         * providerManager会遍历所有securityconfig中注册的provider集合
         * 根据此方法返回true或false来决定由哪个provider
         * 去校验请求过来的authentication
         */
        return (MobileAuthenticationToken.class.isAssignableFrom(authentication));
    }

}

在自定义的service中我们可以校验验证码,设置用户权限等。

@Service
public class MobileUserDetailService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        String password = "";
        Boolean flag = false;
        List<GrantedAuthority> authorities = new ArrayList<>();

        // 根据用户名查询数据库中的用户,判断是否存在;获取缓存中的code等
        // 用户权限列表
        List<String> permissions = new ArrayList<>();
        permissions.add("insert");
        permissions.add("update");
        permissions.add("remove");
        permissions.add("delete");
        List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
        // 缓存中的验证码
        password = "1234";
        // 用户账号状态
        flag = true;
        authorities = grantedAuthorities;

        return new JwtUserDetails(username, password, flag, authorities);
    }

}

4. 完整的配置文件

自定义登录成功以及登录失败的文件看本项目的源码吧

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private CusAuthenticationSuccessHandler successHandler;

    @Autowired
    private CusAuthenticationFailureHandler failureHandler;

    @Autowired
    private MobileAuthenticationProvider mobileAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 配置认证方式等
        auth.authenticationProvider(adminAuthenticationProvider());
        auth.authenticationProvider(mobileAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http相关的配置,包括登入登出、异常处理、会话管理等
        http.cors().and().csrf().disable()
                .authorizeRequests()
                .antMatchers("/mobile/login", "/home/login").permitAll()
                .anyRequest().authenticated();

        //各类错误异常处理 以下针对于访问资源路径 认证异常捕获 和 无权限处理
        http.exceptionHandling().authenticationEntryPoint((req, resp, exception) -> {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            //封装异常描述信息
            String json = JSONObject.toJSONString(exception.getMessage());
            out.write(json);
            out.flush();
            out.close();
        }).accessDeniedHandler((resq, resp, exception) -> {
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            String json = JSONObject.toJSONString("无权限:");
            out.write(json);
            out.flush();
            out.close();
        });

        http.addFilterAfter(externalAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 方式一
     *
     * @return
     */
    @Bean
    public CustomAuthenticationProcessingFilter externalAuthenticationProcessingFilter() {
        CustomAuthenticationProcessingFilter filter = new CustomAuthenticationProcessingFilter();
        filter.setAuthenticationManager(authenticationManager);
        filter.setAuthenticationSuccessHandler(successHandler);
        filter.setAuthenticationFailureHandler(failureHandler);
        return filter;
    }

    /**
     * 方式二
     *
     * @return
     */
    @Bean
    public MobileAuthenticationProcessingFilter mobileAuthenticationProcessingFilter() {
        MobileAuthenticationProcessingFilter mobileFilter = new MobileAuthenticationProcessingFilter(authenticationManager);
        mobileFilter.setAuthenticationSuccessHandler(successHandler);
        mobileFilter.setAuthenticationFailureHandler(failureHandler);
        return mobileFilter;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public AdminAuthenticationProvider adminAuthenticationProvider() {
        AdminAuthenticationProvider adminAuthenticationProvider = new AdminAuthenticationProvider();
        return adminAuthenticationProvider;
    }

    /**
     * 新版本的security规定必须设置一个默认的加密方式
     * 用于登录验证 注册等
     *
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

文章作者: Cody_
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Cody_ !
评论
 上一篇
Docker远程安全连接 Docker远程安全连接
配置Docker远程连接,我们可以配合Docker插件在本地打包就自动上传并创建容器,不用我们再去服务器中手动创建。网上的讲解大多都是直接开放一个端口就直接完事,这样自己玩玩可以,用于正式项目的话不出几个小时就会被扫描到进而被攻击,这次我们
2021-01-09
下一篇 
Elasticsearch数据类型 Elasticsearch数据类型
随着版本的改动,小部分数据类型也有细微的改动,就比如string类型新版本就不再支持。我本地的版本是7.9.3文档版本是7.X 1 常见类型1.1 binary - 二进制型二进制值编码为Base64字符串, 不以默认的方式存储, 不能被搜
2020-12-26
  目录