springscurity实战

springscurity为我们提供了强大的内置功能,但在实际应用场景中依然需要做一定的定制开发和配置。本文尝试通过实战一起了解springscurity的内部世界。

需求场景

混合式开发APP(Hybrid APP)是目前移动互联网主流的前端框架,这样的前端框架对后端接口服务和安全控制有个性化需求,简单整理如下:

  • 动静分离,所有接口返回都是json
  • 无状态restful接口,没有会话保持
  • 手机号做账号,用短信验证码注册登陆
  • APP可以自动登陆
  • 防止暴力破解和短信炸弹,图片验证码
  • 支持公网系统间调用安全认证

针对以上需求,我们需要做以下定制化开发:

  • 增加用户代理主键,实现业务系统用户标识与手机号解耦
  • 登陆后使用JWT token访问接口
  • app原生登陆,跳转webview联合登陆

用户管理

springsecurity为我们提供了完整的用户管理接口和默认实现。UserDetailsManager和UserDetailsService提供了具体的接口约定,实际生产上一般都采用DB作为用户数据持久化方案。所以我们需要关系的核心对象如下:
在这里插入图片描述
springsecurity默认的用户主键是用户账号username,在实际生产系统中为了避免用户账号变更对整个系统数据的影响,需要增加代理主键。为此我们需要重写UserDetailsManager和User对象。
重写UserDetailsManager核心代码如下:

@Override
	protected List<UserDetails> loadUsersByUsername(String username) {
		return getJdbcTemplate().query(this.usersByUsernameQuery,
				new String[] { username }, new RowMapper<UserDetails>() {
					@Override
					public UserDetails mapRow(ResultSet rs, int rowNum)
							throws SQLException {
						Integer id = rs.getInt(1);
						String username = rs.getString(2);
						String password = rs.getString(3);
						boolean enabled = rs.getBoolean(4);
						return new UserAccount(id, username, password,
						enabled, AuthorityUtils.NO_AUTHORITIES);
					}
				});

认证管理

springsecurity认证核心AuthenticationManager默认只有一个实现类ProviderManager,但ProviderManager并不包含真正认证逻辑,而是作为一个代理类调用一组AuthenticationProvider。
认证管理核心对象如下:
在这里插入图片描述

短信验证码登陆

ProviderManager通过supports接口根据AuthenticationToken的类型筛选不同的AuthenticationProvider。AbstractUserDetailsAuthenticationProvider默认使用账号密码认证,为了实现短信验证码认证,我们需要重新实现Authentication和AuthenticationProvider。
核心代码如下:

	public Authentication authenticate(Authentication authentication) throws AuthenticationException {

		SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
		String username = authenticationToken.getPrincipal().toString();

		UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());

		if (user == null) {
			userAccountServiceFacade.register(username);
			user = this.getUserDetailsService().loadUserByUsername(username);
		}
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
		authenticationResult.setDetails(authenticationToken.getDetails());
		return authenticationResult;
	}

springsecurity提供了一个抽象的认证过滤器AbstractAuthenticationProcessingFilter,提供认证服务通用的流程控制能力。
在这里插入图片描述
短信验证码登陆需要独立的接口和处理逻辑,我们通过重写AbstractAuthenticationProcessingFilter,并集成短信验证码认证。
核心代码如下:

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 根据请求参数名,获取请求value
        String mobile = obtainMobile(request);
        String smsCode = obtainSmsCode(request);
        String series = obtainSeries(request);
        
        additionalAuthenticationChecks(mobile,smsCode,series);
        
        // 生成对应的AuthenticationToken
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile,smsCode);

        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

动静分离

在无状态服务中,我们希望用户每次访问受保护资源时,可以不用session或者cookie就可以通过JWT令牌自动认证,所以在登陆成功后要返回access_token。我们可以把用户权限信息封装到令牌中:

    private String doGenerateToken(Map<String, Object> claims, UserAccount userDetails) {
        final Date createdDate = clock.now();
        final Date expirationDate = calculateExpirationDate(createdDate);
        Set<String> roles = AuthorityUtils.authorityListToSet(userDetails.getAuthorities());
        return Jwts.builder()
            .setClaims(claims)
            .setId(userDetails.getId().toString())
            .setSubject(userDetails.getUsername())
            .setIssuedAt(createdDate)
            .setExpiration(expirationDate)
            .claim(ROLE, roles)
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
    }

我们需要定义自己的SuccessHandler:

public class AccessTokenAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
	ObjectMapper om = new ObjectMapper();
	JwtAccessTokenConverter jwtAccessTokenConverter;
	//。。。details
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		UserAccount user = (UserAccount)authentication.getPrincipal();
		String accessToken = jwtAccessTokenConverter.generateToken(user);
		Map<String,Object> result = new HashMap<>();
		//。。。details
        om.writeValue(response.getOutputStream(), result);
	}
}

APP自动登陆

springsecurity提供了RememberMe(记住密码)的功能。为了保证安全性,可以通过数据库存放校验信息实现记住密码登录。核心类库如下:
在这里插入图片描述
混合式开发APP无法统一使用cookie,我们需要重写PersistentTokenBasedRememberMeServices。

	@Override
	protected String extractRememberMeCookie(HttpServletRequest request) {
		return request.getParameter(REFRESH_TOKEN);
	}
	
	@Override
	protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request,
			HttpServletResponse response) {
		String refreshToken = encodeCookie(tokens);
		request.setAttribute(SUCCESS_LOGIN_REFRESH_TOKEN, refreshToken);
	}

退出登陆

退出登陆时需要清空refreshToken.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值