赞
踩
springsecurity是一个功能强大且高度可定制的身份验证和访问控制框架。包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证用户能否访问该系统,一般要求用户提供用户名和密码。用户授权指的是验证某个用户是否有权限执行某个操作,一般来说,系统会为不同的用户分配不同的角色,对应一系列的权限。
Spring Security对Web资源的保护是靠Filter实现的。当初始化Spring Security时,会创建一个名为 springSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过过滤器再到api业务逻辑,下图是Spring Security过滤器链结构图:
整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext。
负责处理我们在登陆页面填写了用户名密码后的登陆请求。其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变。
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
负责权限校验的过滤器。
protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .anyRequest().authenticated() // 所有请求需要身份认证 .and().logout() .logoutUrl("/xxx") .logoutSuccessHandler(logoutSuccessHandler); http.headers().frameOptions().sameOrigin(); http.securityContext().securityContextRepository(contextRepository); http.addFilterBefore(verifyCodeFilter, SecurityContextPersistenceFilter.class); http.addFilterBefore(skipFilter, SecurityContextPersistenceFilter.class); http.addFilterAfter(ExceptionFilter(), TumsExceptionTranslationFilter.class); http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterBefore(appLoginFilter(), UsernamePasswordAuthenticationFilter.class); http.addFilterAt(logoutFilter(), LogoutFilter.class); http.addFilterBefore(tumsUserExpireCheckFilter, FilterSecurityInterceptor.class); }
在指定的beforeFilter之前加入filter,这里的comparator是内置的一个用于比较注册顺序的一个类。
public HttpSecurity addFilterBefore(Filter filter,
Class<? extends Filter> beforeFilter) {
comparator.registerBefore(filter.getClass(), beforeFilter);
return addFilter(filter);
}
进入过滤器UsernamePasswordAuthenticationFilte它继承 AbstractAuthenticationProcessingFilter,执行该方法,解析前端出传的用户名和密码。
//认证 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { ... String username = this.obtainUsername(request); String password = this.obtainPassword(request); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } //认证成功后 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { ... SecurityContextHolder.getContext().setAuthentication(authResult); ... this.successHandler.onAuthenticationSuccess(request, response, authResult); }
过滤器第一步进行认证执行attemptAuthentication方法,用户名和密码封装成UsernamePasswordAuthenticationToken,此时没认证。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
//用户
this.principal = principal;
//密码
this.credentials = credentials;
this.setAuthenticated(false);
}
然后继续调用AbstractUserDetailsAuthenticationProvider接口的authenticate方法去认证,第一步查询用户信息调retrieveUser方法。
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);
...
//查询用户信息
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
...
//比对密码
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
...
//设置认证成功存数据
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
retrieveUser通过loadUserByUsername方法查询用户信息。将用户信息包含权限信息,然后封装成UserDetails返回。
我们可以实现UserDetailsService接口,自定义获取用户信息。
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
UserDetails loadedUser;
...
loadedUser = this.getUserDetailsService().loadUserByUsername(username);
...
}
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
回到authenticate方法,第二步用将查询好封装的UserDetails与前端传入的密码比对,调additionalAuthenticationChecks。
PasswordEncode:数据加密接口
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"));
}
}
}
正确则回到authenticate方法,第三步设置认证成功,调createSuccessAuthentication。
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
这里的UsernamePasswordAuthenticationToken构造方法如下,则从未认证到已认证。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
认证成功后,过滤器进入第二步调successfulAuthentication,将认证信息放入SecurityContext。
同时可以重写该方法,根据需求设置session等。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
}
this.successHandler.onAuthenticationSuccess(request, response, authResult);
}
1.自定义类继承AbstractAuthenticationProcessingFilter,重写attemptAuthentication方法解析前端出传的用户名和密码,认证成功successfulAuthentication方法。
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain
chain, Authentication auth) throws IOException, ServletException {
String username = (String) auth.getPrincipal();
...
TumsSession session = sessionManager.doGetSession(request, response);
session.setUser(userDao.findByUsername(username));
userManager.userInformationSaveToSession(session, username);
super.successfulAuthentication(request, response, chain, auth);
msLog.oper("系统", "登入", "{}登录成功", username);
}
2.userDetailService
自定义类实现接口userDetailService,重写loadUserByUsername方法,通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息包括权限信息,然后封装成UserDetails,并返回。
如:(UserInfo 实现UserDetails接口)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserInfo user = userDao.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
return user;
}
总结:
1.没有认证过的请求重定向到登陆页面进行登录
2.用户输入用户名和密码后请求认证。在继承AbstractAuthenticationProcessingFilter的自定义类解析出用户名和密码封装成UsernamePasswordAuthenticationToken对象,进一步向数据库或缓存查询用户信息,校验密码。
3.校验成功则会保存返回的Authentication到SecurityContext,认证成功。否则会抛出异常,常做的处理是重定向到登陆页面。
4.认证成功后,用户访问资源时会进行权限鉴定。
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中Authentication,然后获取其中的权限信息。判断当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要
1.把当前登录用户的权限信息也存入Authentication。
public class UserInfo implements UserDetails {
...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
2.然后设置我们的资源所需要的权限即可。
//1.通过注解
@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
public String hello(){
return "hello";
}
//2.通过配置类
protected void configure(HttpSecurity http) throws Exception {
http
...
.antMatchers("/testCors").hasAuthority("system:dept:list222")
...
}
private boolean hasAnyAuthorityName(String prefix, String... roles) { //用户拥有的权限 Set<String> roleSet = this.getAuthoritySet(); String[] var4 = roles; int var5 = roles.length; //判断是否包含 for(int var6 = 0; var6 < var5; ++var6) { String role = var4[var6]; String defaultedRole = getRoleWithDefaultPrefix(prefix, role); if (roleSet.contains(defaultedRole)) { return true; } } return false; } private Set<String> getAuthoritySet() { if (this.roles == null) { this.roles = new HashSet(); //调用重写的方法 Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities(); if (this.roleHierarchy != null) { userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities); } this.roles = AuthorityUtils.authorityListToSet(userAuthorities); } return this.roles; }
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
//权限异常
public class TumsAccessDeniedHandler implements AccessDeniedHandler {
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException
accessDeniedException)
throws IOException {
ApiResult result = new ApiResult(UserErrCode.USER_NO_PERMISSION);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().append(new ObjectMapper().writeValueAsString(result));
}
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。