Spring Security运行流程原理分析

Spring Security流程

说白了,它就是一堆的拦截器。下面叙述以下其认证流程。

AbstractAuthenticationProcessingFilter

先看 AbstractAuthenticationProcessingFilter,其是UsernamePasswordAuthenticationFilter的父类。

在其Filter中调用了其子类的验证方法。

	//核心方法
	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
             //调用UsernamePasswordAuthenticationFilter的方法
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) { 
				return;
			}
             //将其认证信息authenticationResult放入session中
			this.sessionStrategy.onAuthentication(authenticationResult, request, response);
			
			if (this.continueChainBeforeSuccessfulAuthentication) {
				chain.doFilter(request, response);
			}
              // 认证成功调用方法
			successfulAuthentication(request, response, chain, authenticationResult);
		}
		catch (InternalAuthenticationServiceException failed) {
			this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
             // 认证失败调用方法
			unsuccessfulAuthentication(request, response, failed);
		}
		catch (AuthenticationException ex) {
			// 认证失败调用方法
			unsuccessfulAuthentication(request, response, ex);
		}
	}

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter,核心拦截器,当有post请求进来时会进如该方法,用户名默认username,密码password

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
	//...
    @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);
	}
    //...
}


UsernamePasswordAuthenticationToken讲解:

该方法有两个构造方法,一个是已经认证成功的,另一个是没有认证成功的。是对表单提交过来的数据进行封装。

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	...
 	
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}
 
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}
    ...
}

AuthenticationManager:

认证接口;常用子类ProviderManager:提供相应的AuthenticationProvider来进行认证

//UsernamePasswordAuthenticationToke认证的返回,点击进入下面方法。
return this.getAuthenticationManager().authenticate(authRequest);
//AuthenticationProvider集合
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();
        //遍历选择对应的AuthenticationProvider然后提供验证。,大致了解即可。
		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);
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) { 
			try {
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
 
			}
			catch (AuthenticationException ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) {
 
				((CredentialsContainer) result).eraseCredentials();
			}
 
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

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

AuthenticationProvider

紧接着我们看AuthenticationProvider类,因为在上述中其提供验证。

AuthenticationProvider接口子类如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oAFXPM0S-1644829572031)(C:\Users\736263\AppData\Roaming\Typora\typora-user-images\image-20211122155740713.png)]

看其中的DaoAuthenticationProvider:

核心代码如下

@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
             //
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
			throw ex;
		}
		catch (Exception ex) {
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

请注意看该段代码

UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);

改代码属于接口UserDetailsService,该接口的实现类为下图,如果我们自己想要查询自己的数据库,只需要实现UserDetailsService重新写loadUserByUsername方法(下示)即可。

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k7zobfUu-1644829572033)(C:\Users\736263\AppData\Roaming\Typora\typora-user-images\image-20211122160314851.png)]

UserDetails

讲解:这个类是系统默认的用户“主体”

// 表示获取登录用户所有权限Collection<? extends GrantedAuthority> getAuthorities();// 表示获取密码String getPassword();// 表示获取用户名String getUsername();// 表示判断账户是否过期boolean isAccountNonExpired();// 表示判断账户是否被锁定boolean isAccountNonLocked();// 表示凭证{密码}是否过期boolean isCredentialsNonExpired();// 表示当前用户是否可用boolean isEnabled();

自定义过滤器:

过滤器链执行顺序(从上到下)

HeaderWriterFilterCsrfFilterLogoutFilterUsernamePasswordAuthenticationFilterBasicAuthenticationFilter...FilterSecurityInterceptor

故如果想要添加权限授权,可以集成BasicAuthenticationFilter该类

eg:

public class TokenAuthFilter extends BasicAuthenticationFilter {    private TokenManager tokenManager;    private RedisTemplate redisTemplate;    public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {        super(authenticationManager);        this.tokenManager = tokenManager;        this.redisTemplate = redisTemplate;    }    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {        //获取当前认证成功用户权限信息        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);        //判断如果有权限信息,放到权限上下文中        if(authRequest != null) {            SecurityContextHolder.getContext().setAuthentication(authRequest);        }        chain.doFilter(request,response);    }    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {        //从header获取token        String token = request.getHeader("token");        if(token != null) {            //从token获取用户名            String username = tokenManager.getUserInfoFromToken(token);            //从redis获取对应权限列表            List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);            Collection<GrantedAuthority> authority = new ArrayList<>();            for(String permissionValue : permissionValueList) {                SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);                authority.add(auth);            }            return new UsernamePasswordAuthenticationToken(username,token,authority);        }        return null;    }}

或者继承GenericFilterBean

public class BeforeLoginFilter extends GenericFilterBean {    @Override    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {        System.out.println("在 UsernamePasswordAuthenticationFilter 前调用");        chain.doFilter(request, response);    }}

之后在配置类中配置即可

  //设置退出的地址和token,redis操作地址    @Override    protected void configure(HttpSecurity http) throws Exception {     	...     	http.addFilter();//添加这句代码即可    	...    }
说明: HttpSecurity 有三个常用方法来配置:addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter) 在 beforeFilter 之前添加 filter addFilterAfter(Filter filter, Class<? extends Filter> afterFilter) 在 afterFilter 之后添加 filteraddFilterAt(Filter filter, Class<? extends Filter> atFilter) 在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter 通过在不同 Filter 的 doFilter() 方法中加断点调试,可以判断哪个 filter 先执行,从而判断 filter 的执行顺序 。 

Spring Security的session和token绑定原理

1.当你在登录的时候,会进入UsernamePasswordAuthenticationFilter,查看其父类AbstractAuthenticationProcessingFilter的源码。注意下面这一句。

如果第一次,则会将其认证信息与session绑定起来,并且只次一份。

//将其认证信息authenticationResult放入session中this.sessionStrategy.onAuthentication(authenticationResult, request, response);

即Authentication被securityContext封装,然后securityContextHolder是将ThreadLocal和securityContext绑定。

 SecurityContextHolder.getContext()//如果有则返回securityContext,如果没有则会创建SecurityContextHolder.getContext().setAuthentication(authRequest);//会将其的认证信息覆盖   

Spring Security整合JWT

当系统登录后重写successfulAuthentication方法,给浏览器返回一个token,

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {    private TokenManager tokenManager;    private RedisTemplate redisTemplate;    private AuthenticationManager authenticationManager;    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {        this.authenticationManager = authenticationManager;        this.tokenManager = tokenManager;        this.redisTemplate = redisTemplate;        this.setPostOnly(false);        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));    }    //1 获取表单提交用户名和密码    @Override    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)            throws AuthenticationException {        //获取表单提交数据        try {            User user = new ObjectMapper().readValue(request.getInputStream(), User.class);            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),                    new ArrayList<>()));        } catch (IOException e) {            e.printStackTrace();            throw new RuntimeException();        }    }    //2 认证成功调用的方法    @Override    protected void successfulAuthentication(HttpServletRequest request,                                             HttpServletResponse response, FilterChain chain, Authentication authResult)            throws IOException, ServletException {        //认证成功,得到认证成功之后用户信息        SecurityUser user = (SecurityUser)authResult.getPrincipal();        //根据用户名生成token        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());        //把用户名称和用户权限列表放到redis        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());        //返回token        ResponseUtil.out(response, R.ok().data("token",token));    }    //3 认证失败调用的方法    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed)            throws IOException, ServletException {        ResponseUtil.out(response, R.error());    }}

然后授权,重写SecurityContext的认证信息,授权。

public class TokenAuthFilter extends BasicAuthenticationFilter {    private TokenManager tokenManager;    private RedisTemplate redisTemplate;    public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {        super(authenticationManager);        this.tokenManager = tokenManager;        this.redisTemplate = redisTemplate;    }    @Override    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {        //获取当前认证成功用户限信息        UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);        //判断如果有权限信息,放到权限上下文中        if(authRequest != null) {            SecurityContextHolder.getContext().setAuthentication(authRequest);        }        chain.doFilter(request,response);    }    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {        //从header获取token        String token = request.getHeader("token");        if(token != null) {            //从token获取用户名            String username = tokenManager.getUserInfoFromToken(token);            //从redis获取对应权限列表            List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);            Collection<GrantedAuthority> authority = new ArrayList<>();            for(String permissionValue : permissionValueList) {                SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);                authority.add(auth);            }            return new UsernamePasswordAuthenticationToken(username,token,authority);        }        return null;    }}

生成token的工具类

public class TokenManager {    //token有效时长    private long tokenEcpiration = 24*60*60*1000;    //编码秘钥    private String tokenSignKey = "123456";    //1 使用jwt根据用户名生成token    public String createToken(String username) {        String token = Jwts.builder().setSubject(username)                .setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration))                .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();        return token;    }    //2 根据token字符串得到用户信息    public String getUserInfoFromToken(String token) {        String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();        return userinfo;    }    //3 删除token    public void removeToken(String token) { }}

spring security推荐使用 BCryptPasswordEncoder 对密码进行加密,只需将其加入spring中即可。

    public static void main(String[] args) {        // 创建密码解析器        BCryptPasswordEncoder bCryptPasswordEncoder = new                BCryptPasswordEncoder();// 对密码进行加密        String atguigu = bCryptPasswordEncoder.encode("123456");// 打印加密之后的数据        System.out.println("加密之后数据:\t"+atguigu);//判断原字符加密后和加密之前是否匹配        boolean result = bCryptPasswordEncoder.matches("123456", atguigu);// 打印比较结果        System.out.println("比较结果:\t"+result);    }

Spring security配置类

public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {    private TokenManager tokenManager;    private RedisTemplate redisTemplate;    private DefaultPasswordEncoder defaultPasswordEncoder;    private UserDetailsService userDetailsService;    @Autowired    public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,                                  TokenManager tokenManager, RedisTemplate redisTemplate) {        this.userDetailsService = userDetailsService;        this.defaultPasswordEncoder = defaultPasswordEncoder;        this.tokenManager = tokenManager;        this.redisTemplate = redisTemplate;    }    /**     * 配置设置     * @param http     * @throws Exception     */    //设置退出的地址和token,redis操作地址    @Override    protected void configure(HttpSecurity http) throws Exception {        http.exceptionHandling()                .authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问                .and().csrf().disable()                .authorizeRequests()                .anyRequest().authenticated()                .and().logout().logoutUrl("/admin/acl/index/logout")//退出路径                .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()                .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))                .addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();    }    //调用userDetailsService和密码处理    @Override    public void configure(AuthenticationManagerBuilder auth) throws Exception 	{        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);    }    //不进行认证的路径,可以直接访问    @Override    public void configure(WebSecurity web) throws Exception {        web.ignoring().antMatchers("/api/**");    }}

开启注释:

@Secured

//判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。 使用注解先要开启注解功能!

@EnableGlobalMethodSecurity(securedEnabled=true,prePostEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled=true)@RequestMapping("testSecured")@ResponseBody@Secured({"ROLE_normal","ROLE_admin"})public String helloUser() {return "hello,user";}

@PreAuthorize

@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用
户的 roles/permissions 参数传到方法中。

先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)@RequestMapping("/preAuthorize")@ResponseBody//@PreAuthorize("hasRole('ROLE_管理员')")@PreAuthorize("hasAnyAuthority('menu:system')")public String preAuthorize(){ System.out.println("preAuthorize");return "preAuthorize";}

@PostAuthorize

@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值
的权限.

先开启注解功能:@EnableGlobalMethodSecurity(prePostEnabled = true)    @RequestMapping("/testPostAuthorize")@ResponseBody@PostAuthorize("hasAnyAuthority('menu:system')")public String preAuthorize(){ System.out.println("test--PostAuthorize");return "PostAuthorize";}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值