27.SpringSecurity-重構用戶名密碼登陸

前言

咱們以前的用戶名/密碼的方式已經能夠正常返回oauth協議的 訪問令牌,也就是access_token.給咱們前端app,前端app拿着這個token就能夠訪問咱們的服務了,接下來咱們分析另外兩種認證方式。前端

  1. 短信驗證碼登陸
  2. 第三方社交方式登陸

內容

1 短信驗證碼登陸

1.1 驗證碼存儲問題

  1. 瀏覽器
    以前咱們的驗證碼是放在session裏面的,用戶點擊驗證碼時候,咱們後臺會生成驗證碼,而後存儲到session裏面,並返回到瀏覽器端,而後登陸時候攜帶此驗證碼進行校驗。

2.App
App端,咱們服務器是沒有session的,這種狀況下會出現什麼問題呢? web

image.png

咱們先獲取短信驗證碼:redis

http://127.0.0.1:8088/code/sms?mobile=13012345678

image.png

獲取到短信驗證碼後登陸:spring

http://127.0.0.1:8088/authentication/mobile  
參數是:  
mobile:13226595347 
smsCode:875653

注意咱們此時不能使用Rest Client去發送短信登陸請求,由於Rest Client基於瀏覽器,而且把cookie攜帶到後端,可是咱們可使用其組織請求參數,而後在dos命令行請求。
組織參數:
image.png數據庫

拷貝參數:
image.png後端

image.png

在dos非瀏覽器窗口請求:(其在app端發送代碼是一致的,沒有cookie) 瀏覽器

image.png

一樣的數據,咱們使用web去發送就會返回200:
image.png服務器

1.2 短信驗證碼登陸

咱們使用短信驗證碼去登陸時候,咱們攜帶一個參數:deviceId;而後咱們會將此參數存儲在數據庫或者redis裏面。而後校驗時候也是帶着這個deviceId,此時咱們去從數據庫查詢。 cookie

image.png

1.2.1驗證碼在不一樣模塊下處理

spring-security-app模塊時候:
咱們在驗證碼存儲,驗證碼校驗和驗證碼移除都是從數據庫存儲中進行操做。而不是直接操做session。 session

spring-security-web模塊時候:
咱們在驗證碼存儲,驗證碼校驗和驗證碼移除都是對Session進行操做。
1.定義一個操做驗證碼的接口
在Spring-Security-Core工程的包:com.yxm.security.core.validate.code下面建立一個接口。

public interface ValidateCodeRepository {
    /**
     * 保存驗證碼
     * @param request
     * @param code
     * @param validateCodeType
     */
    void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType);
    /**
     * 獲取驗證碼
     * @param request
     * @param validateCodeType
     * @return
     */
    ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType);
    /**
     * 移除驗證碼
     * @param request
     * @param validateCodeType
     */
    void remove(ServletWebRequest request, ValidateCodeType validateCodeType);
}

2.spring-security-web定義使用session存儲方式實現ValidateCodeRepository接口的類:

@Component
public class SessionValidateCodeRepository implements ValidateCodeRepository {
    /**
     * 驗證碼放入session時的前綴
     */
    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
    /**
     * 操做session的工具類
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType validateCodeType) {
        sessionStrategy.setAttribute(request,getSessionKey(request, validateCodeType),code);
    }

    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType validateCodeType) {
        return (ValidateCode)sessionStrategy.getAttribute(request,getSessionKey(request,validateCodeType));
    }

    @Override
    public void remove(ServletWebRequest request, ValidateCodeType validateCodeType) {
        sessionStrategy.removeAttribute(request,getSessionKey(request,validateCodeType));
    }

    /**
     * 構建驗證碼放入session時的key
     * @param request
     * @return
     */
    private String getSessionKey(ServletWebRequest request, ValidateCodeType validateCodeType) {
        return SESSION_KEY_PREFIX + validateCodeType.toString().toUpperCase();
    }
}

3.spring-security-app定義使用redis存儲方式實現ValidateCodeRepository接口的類:

@Component
public class RedisValidateCodeRepository implements ValidateCodeRepository {
    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    @Override
    public void save(ServletWebRequest request, ValidateCode code, ValidateCodeType type) {
        redisTemplate.opsForValue().set(buildKey(request, type), code, 30, TimeUnit.MINUTES);
    }
    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType type) {
        Object value = redisTemplate.opsForValue().get(buildKey(request, type));
        if (value == null) {
            return null;
        }
        return (ValidateCode) value;
    }
    @Override
    public void remove(ServletWebRequest request, ValidateCodeType type) {
        redisTemplate.delete(buildKey(request, type));
    }
    /**
     * @param request
     * @param type
     * @return
     */
    private String buildKey(ServletWebRequest request, ValidateCodeType type) {
        String deviceId = request.getHeader("deviceId");
        if (StringUtils.isBlank(deviceId)) {
            throw new ValidateCodeException("請在請求頭中攜帶deviceId參數");
        }
        return "code:" + type.toString().toLowerCase() + ":" + deviceId;
    }
}

當咱們的spring-security-demo項目依賴spring-security-app的時候就會走RedisValidateCodeRepository,當咱們的spring-security-demo項目依賴spring-security-web項目時候就會走SessionValidateCodeRepository實現。

  1. 在Spring-Security-Core工程
    在Spring-Security-Core工程下面咱們在對驗證碼的處理器,處理時候須要保存和移除驗證碼到對應的ValidateCodeRepository中:
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {
    /**
     * 操做session的工具類
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private ValidateCodeRepository validateCodeRepository;

    /**
     * 收集系統中全部的 {@link ValidateCodeGenerator} 接口的實現。
     */
    @Autowired
    private Map<String, ValidateCodeGenerator> validateCodeGenerators;
    @Override
    public void create(ServletWebRequest request) throws Exception {
        //主幹邏輯3步驟:1.生成  2.保存 3.發送(發送是抽象方法,讓子類本身去實現)
        C validateCode = generate(request);
        save(request, validateCode);
        send(request, validateCode);
    }

    /**
     * 生成校驗碼:這裏涉及到Spring裏面的開發技巧;依賴查找
     * 
     * @param request
     * @return
     */
    @SuppressWarnings("unchecked")
    private C generate(ServletWebRequest request) {
        /*String type = getProcessorType(request);*/
        String type = getValidateCodeType(request).toString().toLowerCase();
        String generatorName = type + ValidateCodeGenerator.class.getSimpleName();
        ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
        if(validateCodeGenerator == null){
           throw new ValidateCodeException("驗證碼生成器" + generatorName + "不存在");
        }
        return (C) validateCodeGenerator.generate(request);
    }
    /**
     * 保存校驗碼
     * @param request
     * @param validateCode
     */
    private void save(ServletWebRequest request, C validateCode) {
        ValidateCode code = new ValidateCode(validateCode.getCode(),validateCode.getExpireTime());
        validateCodeRepository.save(request,code,getValidateCodeType(request));
    }
    /**
     * 構建驗證碼放入session時的key
     * @param request
     * @return
     */
    private String getSessionKey(ServletWebRequest request) {
        return SESSION_KEY_PREFIX + getValidateCodeType(request).toString().toUpperCase();
    }
    /**
     * 發送校驗碼,由子類實現
     * @param request
     * @param validateCode
     * @throws Exception
     */
    protected abstract void send(ServletWebRequest request, C validateCode) throws Exception;
    /**
     * 根據請求的url獲取校驗碼類型:根據請求後半段不一樣對應的校驗器也不一樣
     * @param request
     * @return
     */
    private ValidateCodeType getValidateCodeType(ServletWebRequest request){
        String type = StringUtils.substringBefore(getClass().getSimpleName(), "CodeProcessor");
        return ValidateCodeType.valueOf(type.toUpperCase());
    }

    @SuppressWarnings("unchecked")
    @Override
    public void validate(ServletWebRequest request) {
        ValidateCodeType processorType = getValidateCodeType(request);
        String sessionKey = getSessionKey(request);
        //C codeInSession = (C) sessionStrategy.getAttribute(request, sessionKey);
        ValidateCode validateCode = validateCodeRepository.get(request, getValidateCodeType(request));

        String codeInRequest;
        try {
            codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(),
                    processorType.getParamNameOnValidate());
        } catch (ServletRequestBindingException e) {
            throw new ValidateCodeException("獲取驗證碼的值失敗");
        }

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException(processorType + "驗證碼的值不能爲空");
        }

        if (validateCode == null) {
            throw new ValidateCodeException(processorType + "驗證碼不存在");
        }

        if (validateCode.isExpried()) {
            sessionStrategy.removeAttribute(request, sessionKey);
            throw new ValidateCodeException(processorType + "驗證碼已過時");
        }

        if (!StringUtils.equals(validateCode.getCode(), codeInRequest)) {
            throw new ValidateCodeException(processorType + "驗證碼不匹配");
        }
        sessionStrategy.removeAttribute(request, sessionKey);
    }
}

1.3 整改後測試

咱們在請求頭裏面添加字段:deviceId
image.png

查看redis中存儲:
image.png

登陸校驗:
image.png

image.png

相關文章
相關標籤/搜索