Spring Boot後臺腳手架搭建 [五] Spring Security登錄實現以及認證過程

Spring Security實現登錄


spring security的配置

    SecurityConfig

@Override
protected void configure(HttpSecurity http) throws Exception {
   //跨站請求僞造禁用
   http.csrf().disable();

   // 基於token,所以不需要session
   http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
   //不需要校驗的文件
   http.authorizeRequests()
         .antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
               "/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
               "/statics/**")
         .permitAll().anyRequest().authenticated();
   http.formLogin().loginPage("/login.html").loginProcessingUrl("/login")
         .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and()
         .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
   http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
   // 解決不允許顯示在iframe的問題
   http.headers().frameOptions().disable();
   http.headers().cacheControl();

   http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}

    SecurityHandlerConfig(spring security處理器)

/**
 * 登陸成功,返回token
 *
 */
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
    return new AuthenticationSuccessHandler() {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();

            Token token = tokenService.saveToken(loginUser);
            ResponseUtil.responseJson(httpServletResponse, HttpStatus.OK.value(),token);

        }
    };
}

/**
 * 登錄失敗
 */

@Bean
public AuthenticationFailureHandler loginFailureHandler() {
    return new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            String msg = null;
            if (e instanceof BadCredentialsException) {
                msg = "密碼錯誤";
            } else {
                msg = e.getMessage();
            }
            ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
            ResponseUtil.responseJson(httpServletResponse, HttpStatus.UNAUTHORIZED.value(), info);
        }
    };
}

/**
 * 未登錄
 */
@Bean
public AuthenticationEntryPoint authenticationEntryPoint() {
    return new AuthenticationEntryPoint() {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
            ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", "請先登錄");
            ResponseUtil.responseJson(httpServletResponse, HttpStatus.UNAUTHORIZED.value(), info);
        }
    };
}

/**
 * 退出處理
 */

@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
    return new LogoutSuccessHandler() {
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
            ResponseInfo info = new ResponseInfo(HttpStatus.OK.value() + "", "退出成功");

            String token = TokenFilter.getToken(httpServletRequest);
            tokenService.deleteToken(token);
            ResponseUtil.responseJson(httpServletResponse,HttpStatus.OK.value(),info);
        }
    };
}

Token攔截器

@Component
public class TokenFilter extends OncePerRequestFilter {
    private static final String TOKEN_KEY = "token";
    private static final Long MINUTES_10 = 10 * 60 * 1000L;
    @Autowired
    private TokenService tokenService;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = getToken(request);
        if (StringUtils.isBlank(token)) {
            LoginUser loginUser = tokenService.getLoginUser(token);
            if (loginUser != null) {
                loginUser = checkLoginTime(loginUser);
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);

    }

    /**
     * 校驗過期時間
     * 過期時間與當前時間對比,臨近過期10分鐘的話,自動刷新緩存
     *
     * @return
     */

    private LoginUser checkLoginTime(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MINUTES_10) {
            String token = loginUser.getToken();
            loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
            loginUser.setToken(token);
            tokenService.refresh(loginUser);
        }
        return loginUser;
    }

    /**
     * 根據參數或者header獲得token
     * @param request
     * @return
     */

    public static String getToken(HttpServletRequest request) {
        String token = request.getParameter(TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            token = request.getHeader(TOKEN_KEY);
        }
        return token;
    }
}

對token進行驗證攔截

具體實現

前端調用登錄方法

function login(obj) {
   $(obj).attr("disabled", true);

   var username = $.trim($('#username').val());
   var password = $.trim($('#password').val());
   if (username == "" || password == "") {
      $("#info").html('用戶名或者密碼不能爲空');
      $(obj).attr("disabled", false);
   } else {
      $.ajax({
         type : 'post',
         url : '/login',
         data : $("#login-form").serialize(),
         success : function(data) {
            localStorage.setItem("token", data.token);
            location.href = '/index.html';
         },
         error : function(xhr, textStatus, errorThrown) {
            var msg = xhr.responseText;
            var response = JSON.parse(msg);
            $("#info").html(response.message);
            $(obj).attr("disabled", false);
         }
      });

   }
}

因爲在這裏我們配置了登錄請求這裏被攔截下來了。

http.formLogin().loginPage("/login.html").loginProcessingUrl("/login")
      .successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and()
      .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);

後經過

org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

這個攔截器的attemptAuthentication方法獲取到前端的用戶名和密碼

 public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }

    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());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

該方法只支持post請求,獲取到用戶名密碼後構造未認證的UsernamePasswordAuthentication 後設置details然後通過AuthenticationManager(實際上爲ProviderManager的authenticate方法)完成驗證

 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        Iterator var6 = this.getProviders().iterator();

        while(var6.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var6.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            this.eventPublisher.publishAuthenticationSuccess(result);
            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            this.prepareException((AuthenticationException)lastException, authentication);
            throw lastException;
        }
    }

    該方法先循環遍歷provider 找到具體執行該認證的provider 然後複製details 然後由具體的provider來完成認證

    

    具體的驗證處理由DaoAuthenticationProvider的父類AbstractUserDetailsAuthenticationProvider的authenticate方法來完成

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

        該方法先獲取到登錄的用戶名,如果緩存中有UserDetails則從緩存中獲取UserDetails如果沒有則根據用戶名和authentication獲取UserDetails 後進行一系列驗證成功後返回Authentication

驗證過程由DaoAuthenticationProvider的retrieveUser和additionalAuthenticationChecks方法來實現

 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }
 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    這個方法通過loadUserByUsername來獲取到數據庫中的用戶信息,所以我們要自己重寫實現UserDetailsService接口的loadUserByUsername方法

@Service
public class UserDetailsServiceImpl implements UserDetailsService{


    @Autowired
    private UserService userService;
    @Autowired
    private PermissionDao permissionDao;


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

        SysUser user = userService.getUser(username);
        if (user == null) {
            throw new AuthenticationCredentialsNotFoundException("用戶名不存在");
        } else if (user.getStatus() == Status.LOCKED) {
            throw new LockedException("用戶被鎖定,請聯繫管理員");
        } else if (user.getStatus() == Status.DISABLED) {
            throw new DisabledException("用戶已被封禁");
        }

        LoginUser loginUser = new LoginUser();
        BeanUtils.copyProperties(user, loginUser);

        List<Permission> permissions = permissionDao.listByUserId(user.getId());
        loginUser.setPermissions(permissions);

        return loginUser;
    }
}

通過loadUserByUsername獲取到數據庫的用戶信息在通過上面的兩個方法和前端傳過來的用戶信息進行比對就完成了登錄認證。

認證完成後成功失敗等一系列處理在SecurityHandlerConfig進行處理,成功後將token儲存到redis中。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章