添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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();