springsecurity講解2

本文用示例的方式講解,springsecurity,使用session方式,
用戶名密碼和手機驗證碼兩種方式
非常簡陋的登入頁面


image.png

該示例的代碼


image.png

CustomAuthenticationFailureHandler 失敗處理器
/**
認證失敗處理器
 **/
@Component
public class CustomAuthenticationFailureHandler  extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException{
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("認證失敗");
    }
}

CustomAuthenticationSuccessHandler 成功處理器

/**
認證成功處理器
 **/
@Component
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException{
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("認證成功");
    }
}

CustomUserDetailsService 獲取用戶信息

/**
 *
 * 模擬從數(shù)據(jù)庫獲取用戶信息,這里要注意的是,如果在配置的時候使用內(nèi)存的方式,是不回使用該services
 * SpringSecurityConfiguration方法中規(guī)定了使用那種方式管理用戶信息,本例使用的是內(nèi)存的方式
 * 所以在用戶名密碼模式的時候,不回執(zhí)行l(wèi)oadUserByUsername,手機登入的時候還是會走loadUserByUsername方法
 */
@Configuration
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        //封裝用戶信息,用戶名和密碼和權(quán)限,注意這里要注意密碼應(yīng)該是加密的
        //省略從數(shù)據(jù)庫獲取詳細信息
        return new User(username, "1234",
                AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
    }
}

SpringSecurityConfiguration security整體配置


@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    SecurityConfigurerAdapter mobileAuthenticationConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login.html","/code/mobile").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .successHandler(new CustomAuthenticationSuccessHandler())
                .failureHandler(new CustomAuthenticationFailureHandler())
                .loginPage("/login")
        ;  //瀏覽器以form表單形式
        //將手機驗證碼配置放到http中,這樣mobileAuthenticationConfig配置就會生效
        http.apply(mobileAuthenticationConfig);

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        // 用戶信息存儲在內(nèi)存中
        auth.inMemoryAuthentication().withUser("user")
            .password(new BCryptPasswordEncoder().encode("1234")).authorities("ADMIN");
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/code/mobile");
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        // 官網(wǎng)建議的加密方式,相同的密碼,每次加密都不一樣,安全性更好一點
        return new BCryptPasswordEncoder();
    }
}


CacheValidateCode 手機驗證碼的內(nèi)存存儲

/**
 * 將手機驗證碼保存起來,后續(xù)驗證中,實際項目中要放到redis等存儲
 **/
public class CacheValidateCode {
    public static ConcurrentHashMap<String, String> cacheValidateCodeHashMap = new ConcurrentHashMap();

}

MobileAuthenticationConfig 手機驗證碼配置類,在SpringSecurityConfiguration中通過http.apply方式放到springsecurity中

/**
 * 用于組合其他關(guān)于手機登錄的組件
 */
@Component
public class MobileAuthenticationConfig
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Autowired
    CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    @Autowired
    UserDetailsService mobileUserDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception{

        MobileAuthenticationFilter mobileAuthenticationFilter = new MobileAuthenticationFilter();
        // 獲取容器中已經(jīng)存在的AuthenticationManager對象,并傳入 mobileAuthenticationFilter 里面
        mobileAuthenticationFilter.setAuthenticationManager(
                http.getSharedObject(AuthenticationManager.class));


        // 傳入 失敗與成功處理器
        mobileAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        mobileAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        // 構(gòu)建一個MobileAuthenticationProvider實例,接收 mobileUserDetailsService 通過手機號查詢用戶信息
        MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
        provider.setUserDetailsService(mobileUserDetailsService);

        // 將provider綁定到 HttpSecurity上,并將 手機號認證過濾器綁定到用戶名密碼認證過濾器之后
        http.authenticationProvider(provider)
            .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

MobileAuthenticationFilter 手機驗證filter,完全模仿UsernamePasswordAuthenticationFilter

/**
 * 用于校驗用戶手機號是否允許通過認證
 * 完全復(fù)制 UsernamePasswordAuthenticationFilter
 */
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private String mobileParameter = "mobile";
    private String validateCodeParameter = "code";
    private boolean postOnly = true;


    public MobileAuthenticationFilter(){
        super(new AntPathRequestMatcher("/mobile/form", "POST"));
    }

    // ~ Methods
    // ========================================================================================================

    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException{
        if(postOnly && !request.getMethod().equals("POST")){
            throw new AuthenticationServiceException(
                    "Authentication method not supported: "+request.getMethod());
        }

        String mobile = obtainMobile(request);
        String validateCode = obtainValidateCode(request);

        if(mobile == null){
            mobile = "";
        }

        mobile = mobile.trim();

        MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile, validateCode);

        // sessionID, hostname
        setDetails(request, authRequest);
        //認證手機碼是否正確,通過provider的方式處理,使用哪個provider,是根據(jù)authRequest是哪個類型的token
        //這里放的是MobileAuthenticationToken
        return this.getAuthenticationManager().authenticate(authRequest);
    }


    /**
     * 從請求中獲取手機號碼
     */
    @Nullable
    protected String obtainMobile(HttpServletRequest request){
        return request.getParameter(mobileParameter);
    }

    /**
     * 從請求中獲取驗證碼
     */
    @Nullable
    protected String obtainValidateCode(HttpServletRequest request){
        return request.getParameter(validateCodeParameter);
    }

    /**
     * 將 sessionID和hostname添加 到MobileAuthenticationToken
     */
    protected void setDetails(HttpServletRequest request,
                              MobileAuthenticationToken authRequest){
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }


    /**
     * 設(shè)置是否為post請求
     */
    public void setPostOnly(boolean postOnly){
        this.postOnly = postOnly;
    }

    public String getMobileParameter(){
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter){
        this.mobileParameter = mobileParameter;
    }
}

MobileAuthenticationProvider 手機驗證處理器

/**
 * 手機認證處理提供者,要注意supports方法和authenticate
 * supports判斷是否使用當(dāng)前provider
 * authenticate 驗證手機驗證碼是否正確
 *
 */
public class MobileAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public void setUserDetailsService(UserDetailsService userDetailsService){
        this.userDetailsService = userDetailsService;
    }

    /**
     * 認證處理:
     * 1. 通過手機號碼 查詢用戶信息( UserDetailsService實現(xiàn))
     * 2. 當(dāng)查詢到用戶信息, 則認為認證通過,封裝Authentication對象
     *
     * @param authentication
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException{
        MobileAuthenticationToken mobileAuthenticationToken =
                ( MobileAuthenticationToken ) authentication;
        // 獲取手機號碼
        String mobile = ( String ) mobileAuthenticationToken.getPrincipal();
        String validateCodeParameter = ( String ) mobileAuthenticationToken.getCredentials();
        // 通過 手機號碼 查詢用戶信息( UserDetailsService實現(xiàn))
        UserDetails userDetails =
                userDetailsService.loadUserByUsername(mobile);
        mobileAuthenticationToken.setDetails(userDetails);
        // 未查詢到用戶信息
        if(userDetails == null){
            throw new AuthenticationServiceException("該手機號未注冊");
        }
        // 1. 判斷 請求是否為手機登錄,且post請求
        try{
            // 校驗驗證碼合法性
            validate(mobile, validateCodeParameter);
        }catch(AuthenticationException e){
           throw new AuthenticationServiceException(e.getMessage());
        }
        //最終返回認證信息,這里要注意的是,返回的token中的authenticated字段要賦值為true
        return createSuccessAuthentication(mobileAuthenticationToken);
    }

    /**
     * 通過這個方法,來選擇對應(yīng)的Provider, 即選擇MobileAuthenticationProivder
     *
     * @param authentication
     * @return
     */
    @Override
    public boolean supports(Class<?> authentication){
        return MobileAuthenticationToken.class.isAssignableFrom(authentication);
    }


    private void validate(String mobile, String inpuCode){
        // 判斷是否正確
        if(StringUtils.isEmpty(inpuCode)){
            throw new AuthenticationServiceException("驗證碼不能為空");
        }
        String cacheValidateCode = CacheValidateCode.cacheValidateCodeHashMap.get(mobile);
        if(!inpuCode.equalsIgnoreCase(cacheValidateCode)){
            throw new AuthenticationServiceException("驗證碼輸入錯誤");
        }
    }

    protected Authentication createSuccessAuthentication(
            Authentication authentication){
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        MobileAuthenticationToken result = new MobileAuthenticationToken(
                authentication.getPrincipal(), authentication.getCredentials(),
                AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
        result.setDetails(authentication.getDetails());
        return result;
    }
}

MobileAuthenticationToken 手機驗證碼的token

/**
 * 創(chuàng)建自己的token,參考UsernamePasswordAuthenticationToken
 */
public class MobileAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    // ================================================================================================

    private final Object principal;
    private Object credentials;

    // ~ Constructors
    // ===================================================================================================

    /**
     * This constructor can be safely used by any code that wishes to create a
     * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
     * will return <code>false</code>.
     */
    public MobileAuthenticationToken(Object principal, Object credentials){
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    /**
     * This constructor should only be used by <code>AuthenticationManager</code> or
     * <code>AuthenticationProvider</code> implementations that are satisfied with
     * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
     * authentication token.
     *
     * @param principal
     * @param credentials
     * @param authorities
     */
    public MobileAuthenticationToken(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
    }

    // ~ Methods
    // ========================================================================================================

    public Object getCredentials(){
        return this.credentials;
    }

    public Object getPrincipal(){
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException{
        if(isAuthenticated){
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials(){
        super.eraseCredentials();
        credentials = null;
    }
}

controller

@Controller
public class Congtroller {
    @RequestMapping("/code/mobile")
    @ResponseBody
    public String mobileCode(HttpServletRequest request){
        // 1. 生成一個手機驗證碼
        String code = RandomStringUtils.randomNumeric(4);
        // 2. 將手機獲取的信息保存到緩存里,實際應(yīng)用中,可以放到redis中
        String mobile = request.getParameter("mobile");
        CacheValidateCode.cacheValidateCodeHashMap.put(mobile, code);
        System.out.println("手機驗證碼"+code);
        return code;
    }
}

login.html 登入頁,十分簡單

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登錄頁</title>
</head>
<script src="https://upcdn.b0.upaiyun.com/libs/jquery/jquery-2.0.2.min.js"></script>

<body>

<form action="http://127.0.0.1:8080/login"method="post">
    <label for="username">用戶名:</label>
    <input type="text" name="username" id="username">

    <label for="password">密 碼:</label>
    <input type="password" name="password" id="password">
    <button type="submit">登錄</button>
</form>
<form action="http://127.0.0.1:8080/mobile/form"method="post">
    <label for="mobile">手機號:</label>
    <input type="text" name="mobile" id="mobile">

    <label for="sendCode">驗證碼:</label>
    <input type="text" name="code" id="sendCode">
    <button type="submit">登錄</button>
</form>
<button onclick="sendCode()"> 獲取驗證碼 </button>
<script>
    function sendCode() {
        $.ajax(
            {
                type: "post",
                url: "http://127.0.0.1:8080/code/mobile",
                data: $("#mobile").serialize(),
                success: function (result) {
                    alert(result);
                }
            }
        )
    }
</script>
</body>
</html>

思路非常簡單,就是定義了關(guān)于手機的驗證filter,并放到security中,在通過驗證碼登入的時候,首先創(chuàng)建MobileAuthenticationToken,遍歷所有的provider的時候,通過support方法獲取到使用哪個provider,MobileAuthenticationProvider手機驗證provider,驗證手機號的驗證碼是否正確,如果正確就將MobileAuthenticationToken放到SecurityContextHolder中,保存在ThreadLocal變量中,該線程就能使用了,并且將MobileAuthenticationToken的authenticated設(shè)置為true,在security的最后一個攔截器FilterSecurityInterceptor判斷是都已經(jīng)驗證過了,并且判斷角色是否可以訪問當(dāng)前接口,
這樣就是驗證的整個流程,session的方式驗證,在登入成功的時候token放到tomcat的內(nèi)存中了,key就是sessionid,前端將session傳到server時,從tomcat中獲取已經(jīng)驗證過的token,這樣就實現(xiàn)了登入后,其他接口可以正常訪問的流程.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容