Spring Security 分享会

(本篇是公司每周的后台技术分享会讲解稿,可能其他人直接看会有一定障碍,所以,谨慎阅读!)

说在前面

首先本人接触这个框架并不久,记得刚进来的时候导师也跟我把流程讲了一遍,因为以前项目中没用过这个东西,听的一个懵啊。最近刚好做到一些登录的需求,也正好趁着有兴趣学习了一段时间,今天就斗胆来分享一波。然后本次分享的目标不是整个框架的源码,更多的是在登录流程方面的讲解。本次分享应该会比较实用,毕竟大家都能接触到。

预期目标

  • 初级:熟悉Spring Security在项目中的运用
  • 中级:熟悉源码层面登录流程
  • 高级:通过看源码的方式解决一些问题
  • 附加:授权控制

什么是 Spring Security?

官方介绍

  • Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一个功能强大且高度可定制的身份认证和访问控制框架,它是保护基于spring的应用程序的事实标准。

  • Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.

Spring Security是一个重点为Java应用程序提供认证和授权的框架。与所有Spring项目一样,Spring Security的真正强大之处在于它可以很容易地扩展以满足定制需求。

通俗来讲

  • 首先我们前端应用访问后台资源,比如后台接口,是需要带上访问凭证(令牌)的,我们肯定不能够直接将接口资源暴露给前端应用,这是有很大安全隐患的。
  • 这个框架就是方便了获取凭证流程,其重点在于认证和授权,认证就是对你的身份进行认证,比如校验用户名密码是否正确、手机号是否正确、是否在我们库中存在该手机号用户,认证的目的就是为了授权,授权的结果就是给到前端一个访问凭证,比如给到前端一个JWT令牌。
  • 前端有了这个令牌,每次请求带上令牌就可以访问后台资源了,当然每次访问资源之前,后台都会对这个令牌进行一个校验,判断这个令牌是否有效是否已过期等等。

Spring Security在项目中的运用

  • 基于公司项目,以短信验证码登录为例

需要创建的类

  • 创建过滤器,过滤拦截登录请求,一般可以继承AbstractAuthenticationProcessingFilter,如:
public class VipMemberAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
    

	public VipMemberAuthenticationFilter() {
    
    
		super(new AntPathRequestMatcher("/vip/members:login", "POST"));
	}
	private final String sessionCachePrefix = "vip:s";

	@Override
	public void afterPropertiesSet() {
    
    
		Assert.notNull(getAuthenticationManager(), "AuthenticationManager must be specified");
		Assert.notNull(getSuccessHandler(), "AuthenticationSuccessHandler must be specified");
		Assert.notNull(getFailureHandler(), "AuthenticationFailureHandler must be specified");
	}

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws org.springframework.security.core.AuthenticationException, IOException, ServletException {
    
    

		String body = StreamUtils.copyToString(request.getInputStream(), Charset.forName("UTF-8"));
		String mobile = null, smsCode = null;
		if(StringUtils.hasText(body)) {
    
    
		    JSONObject jsonObj = JSON.parseObject(body);
		    mobile = jsonObj.getString("mobile");
		    smsCode = jsonObj.getString("smsCode");
		}

		// 验证短信验证码是否正确
		if (StringUtils.isEmpty(mobile)) {
    
    
			throw AuthenticationException.USERNAME_IS_NULL;
		}
		String verifyCodeInCache = JedisUtil.getJedisInstance().execGetFromCache(sessionCachePrefix + ":" + mobile + ":svc");
		if (StringUtils.isEmpty(smsCode) || !smsCode.equalsIgnoreCase(verifyCodeInCache)) {
    
    
			throw AuthenticationException.VERIFY_CODE_INVALID;
		}

		VipMemberUsernamePasswordAuthenToken authRequest = new VipMemberUsernamePasswordAuthenToken(
				mobile.trim(), smsCode);
		
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}
  • 创建Provider,用来认证登录请求,实现AuthenticationProvider,所有的Provider由AuthenticationManager来管理,如:
public class VipMemberJsonAuthenticationProvider implements AuthenticationProvider{
    
    

	private VipMemberFacade vipMemberFacade;
	public VipMemberJsonAuthenticationProvider(VipMemberFacade vipMemberFacade) {
    
    
		this.vipMemberFacade = vipMemberFacade;
	}

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    
    

		String mobile = authentication.getPrincipal().toString();
		VipMemberQueryRequest vipMemberQueryRequest = new VipMemberQueryRequest();
		vipMemberQueryRequest.setMobile(mobile);
		vipMemberQueryRequest.setDeleted(false);
		vipMemberQueryRequest.setStatusList(Arrays.asList(MemberStatus.ENABLE.getCode(), MemberStatus.MANUAL_BLOCK.getCode(), MemberStatus.SYSTEM_BLOCK.getCode()));
		PageCommonResponse<VipMemberDTO> pageResp = vipMemberFacade.queryMember(vipMemberQueryRequest);
		//去除掉用户不存在的错误码
		if (!Constants.OPERATE_SUCCESS.equals(pageResp.getCode())) {
    
    
			throw new AuthenticationException(pageResp.getCode(), pageResp.getEngDesc(), pageResp.getChnDesc());
		}
		
		// balabala... 一堆封装用户信息代码

		//重新登录的时候去除掉缓存的用户信息
		JedisUtil.getJedisInstance().execDelToCache(com.meicloud.mcu.sria.wxclient.constant.Constants.SYS_USER_CACHE_PREFIX + vipMemberDTO.getId() + "_BA");

		LoginDTO loginDTO = new LoginDTO();
		loginDTO.setId(vipMemberDTO.getId());

		loginDTO.setBelongToId(vipMemberDTO.getVipMemberBelongToDTO().getBelongToId());
		JwtAuthenticationToken token = new JwtAuthenticationToken(loginDTO, null, null);
		return token;
	}

	@Override
	public boolean supports(Class<?> authentication) {
    
    
		return authentication.isAssignableFrom(VipMemberUsernamePasswordAuthenToken.class);
	}
}
  • 创建登录成功处理器和失败处理器,分别实现AuthenticationSuccessHandlerAuthenticationFailureHandler接口,如:
/**
 * 成功处理器,这里主要生成一个JWT,返回给前端
 */
public class VipMemberJsonLoginSuccessHandler implements AuthenticationSuccessHandler{
    
    

	private SecurityConfig securityConfig;
	public VipMemberJsonLoginSuccessHandler(SecurityConfig securityConfig) {
    
    
		this.securityConfig = securityConfig;
	}

	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
    
    

		LoginDTO user = (LoginDTO)authentication.getPrincipal();
		user.setUserType("BA");
		Date date = new Date(System.currentTimeMillis() + securityConfig.getTokenExpireTimeInSecond() * 1000);

		Algorithm algorithm = Algorithm.HMAC256(securityConfig.getTokenEncryptSalt());

		String token = JWT.create()
				.withSubject(user.getId()+"_"+"BA")
				.withClaim(LoginDTO.BELONG_TO_ID, user.getBelongToId())
				.withExpiresAt(date)
				.withIssuedAt(new Date())
				.sign(algorithm);

		BaseResponse<LoginDTO> baseResponse = new BaseResponse<>();
		baseResponse.setCode(Constants.OPERATE_SUCCESS);
		baseResponse.setEngDesc("success");
		baseResponse.setChnDesc("登录成功");
		baseResponse.setContent(user);
		response.setHeader(securityConfig.getTokenName(), token);
		response.setCharacterEncoding("UTF-8");
		response.getWriter().print(JSON.toJSONString(baseResponse));
	}
}

/**
 * 失败处理器,返回一些错误码和错误提示
 */
public class HttpStatusLoginFailureHandler implements AuthenticationFailureHandler{
    
    

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
										org.springframework.security.core.AuthenticationException exception) throws IOException, ServletException {
    
    

		BaseResponse baseResponse = new BaseResponse();

		if (exception instanceof AuthenticationException) {
    
    
			AuthenticationException e = (AuthenticationException)exception;
			baseResponse.setCode(e.getCode());
			baseResponse.setChnDesc(e.getChnDesc());
			baseResponse.setEngDesc(e.getEngDesc());
		} else {
    
    
			baseResponse.setEngDesc(exception.getMessage());
		}

		if (AuthenticationException.JWT_EXPIRED.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.JWT_IS_EMPTY.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.USER_NOT_EXISTS.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.USER_DISABLED.getCode().equals(baseResponse.getCode())
				|| AuthenticationException.JWT_FORMAT_ERROR.getCode().equals(baseResponse.getCode())) {
    
    
			response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
		}

		response.setContentType("application/json");
		response.setCharacterEncoding(Charset.defaultCharset().displayName());
		String responseText = JSON.toJSONString(baseResponse);
		response.getWriter().print(responseText);
	}
}
  • 创建认证Token,需要实现Authentication接口,当然一般情况下我们继承它的实现类,其作用是用来封装认证令牌,如:
/**
 * 可以继承一些类方便我们使用,当然最终肯定需要实现Authentication接口
 */
public class VipMemberUsernamePasswordAuthenToken extends UsernamePasswordAuthenticationToken {
    
    

    public VipMemberUsernamePasswordAuthenToken(Object principal, Object credentials) {
    
    
        super(principal, credentials);
    }

    @Override
    public boolean implies(Subject subject) {
    
    
        return super.implies(subject);
    }
}

需要添加的配置

  • 配置过滤器,即把过滤器加到过滤器链上,同时配置登录成功处理器和失败处理器,创建一个配置类,实现AbstractHttpConfigurer,如:
public class JsonLoginConfigurer<T extends JsonLoginConfigurer<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B>  {
    
    

	private SysUserFacade sysUserFacade;
	private SecurityConfig securityConfig;

	public JsonLoginConfigurer(SysUserFacade sysUserFacade, SecurityConfig securityConfig) {
    
    
		this.sysUserFacade = sysUserFacade;
		this.securityConfig = securityConfig;
	}

	@Override
	public void configure(B http) throws Exception {
    
    
		VipMemberAuthenticationFilter vipMemberAuthFilter = new VipMemberAuthenticationFilter();
		vipMemberAuthFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
		vipMemberAuthFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
		// 登录成功处理器
		vipMemberAuthFilter.setAuthenticationSuccessHandler(new VipMemberJsonLoginSuccessHandler(sysUserFacade, securityConfig));
		// 登录失败处理器
		vipMemberAuthFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
		// 拦截器位置
		VipMemberAuthenticationFilter vipMemberFilter = postProcess(vipMemberAuthFilter);
		http.addFilterAfter(vipMemberFilter, LogoutFilter.class);
	}
}
  • 主配置类,一般可以继承WebSecurityConfigurerAdapter,其实所有配置都是在这配的,上面的过滤器配置也要apply进这里,注意这里还要将我们定义的Provider进行配置,其实就是让AuthenticationManager进行管理。如:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
    
    
	@Override
	protected void configure(HttpSecurity http) throws Exception {
    
    
		http.authorizeRequests()
				.antMatchers(securityConfig.getPermitUrls()).permitAll()
				.anyRequest().authenticated()
		        .and()
		    .csrf().disable()
		    .sessionManagement().disable()
		    .cors()
		    .and()
		    .headers().addHeaderWriter(new StaticHeadersWriter(Arrays.asList(
		    		new Header("Access-control-Allow-Origin","*"))))
		    .and()
		    .addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
		    .apply(new JsonLoginConfigurer<>(sysUserFacade, securityConfig))
		    .and()
		    .apply(new JwtLoginConfigurer<>(sysUserFacade, vipMemberFacade, securityConfig))
		    .and()
		    .logout()
		        .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
		    .and()
		    .sessionManagement().disable();
	}
}

源码层面登录流程讲解

流程描述

  • 其实整个认证授权流程无非就是走一个过滤器(链),中间抛出异常就调用异常处理器返回给前端一些错误码和报错信息,没有异常就调用成功处理器构建一个JWT授权令牌给到前端。

流程说明

  • 第一步:用户点击获取短信验证码接口获取验证码,同时后台把这个验证码存进Redis
  • 第二步:用户点击登录,发起登录请求,请求被VipMemberAuthenticationFilter拦截,会调用过滤器的doFilter方法
  • 第三步:doFilter方法会调用attemptAuthentication方法,方法里面会先进行最基本的校验,比如手机号是否为空、验证码是否正确,验证无误之后会构建一个VipMemberUsernamePasswordAuthenToken封装认证用的资产,比如这里是手机号,然后调用AuthenticationManagerauthenticate方法继续认证。
  • 第四步:进入AuthenticationManager的authenticate方法,它会循环所有的Providers,看看哪个Provider支持认证VipMemberUsernamePasswordAuthenToken,支持的话就调用对应Provider的authenticate进行认证。
  • 第五步:经过判断之后会调用VipMemberJsonAuthenticationProviderauthenticate方法,方法里面进行数据库层的认证,比如根据手机号能否查询到用户、查询到的用户是否被禁用等。
  • 第六步:上一步认证成功之后会返回带有用户信息且已认证成功的Token,返回到第四步AuthenticationManager的authenticate方法,再进行各种判断或处理后,继续返回到第三步的doFilter,然后再调用successfulAuthentication方法,方法里面主要保存用户信息到SecurityContext,再就是调用我们定义的成功处理器。
  • 第七步:成功处理器VipMemberJsonLoginSuccessHandler主要目的就是构建令牌给到前端,至此整个登录流程结束。

JWT令牌简介

  • 全称:Json Web Token
  • 特点:
    • 自包含,即自身包含了一些用户信息,和以前的随机串(JSSESSIONID)相比,以前JSSESSIONID方式缓存丢失后,登录信息就丢失了,而JWT可以通过自身包含的信息重新查出用户数据。
    • 密签,加salt进行签名,防止别人修改。
    • 可扩展,想放什么信息都可以放,但是一般不放敏感信息,因为只要有令牌都是可以查看里面信息的。

通过看源码的方式解决一些问题

  • 创建并配置自定义的AuthenticationEventPublisher来实现一些自定义的登录事件发布。

授权控制

  • 根据登录角色不同,根据角色访问权限限制接口的访问。

讨论一个问题

  • 我们知道在登录成功之后,会把用户信息保存到SecurityContext中,其实可以知道内容真正保存在了ThreadLocal本地线程变量中,而如果请求结束不清掉的话会导致用户信息一直存在那个线程中,假如说其他用户使用该线程发起另一个请求,就会获取到上一个用户的信息,所以问题是:ThreadLocal中的用户信息是什么时候清掉的?

猜你喜欢

转载自blog.csdn.net/qq_36221788/article/details/106697171
今日推荐