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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章