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中。