springsecurity講解2

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


image.png

該示例的代碼


image.png

CustomAuthenticationFailureHandler 失敗處理器
/**
認(rèn)證失敗處理器
 **/
@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("認(rèn)證失敗");
    }
}

CustomAuthenticationSuccessHandler 成功處理器

/**
認(rèn)證成功處理器
 **/
@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("認(rèn)證成功");
    }
}

CustomUserDetailsService 獲取用戶信息

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
        //封裝用戶信息,用戶名和密碼和權(quán)限,注意這里要注意密碼應(yīng)該是加密的
        //省略從數(shù)據(jù)庫獲取詳細(xì)信息
        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表單形式
        //將手機(jī)驗(yàn)證碼配置放到http中,這樣mobileAuthenticationConfig配置就會(huì)生效
        http.apply(mobileAuthenticationConfig);

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        // 用戶信息存儲(chǔ)在內(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)建議的加密方式,相同的密碼,每次加密都不一樣,安全性更好一點(diǎn)
        return new BCryptPasswordEncoder();
    }
}


CacheValidateCode 手機(jī)驗(yàn)證碼的內(nèi)存存儲(chǔ)

/**
 * 將手機(jī)驗(yàn)證碼保存起來,后續(xù)驗(yàn)證中,實(shí)際項(xiàng)目中要放到redis等存儲(chǔ)
 **/
public class CacheValidateCode {
    public static ConcurrentHashMap<String, String> cacheValidateCodeHashMap = new ConcurrentHashMap();

}

MobileAuthenticationConfig 手機(jī)驗(yàn)證碼配置類,在SpringSecurityConfiguration中通過http.apply方式放到springsecurity中

/**
 * 用于組合其他關(guān)于手機(jī)登錄的組件
 */
@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對(duì)象昂芜,并傳入 mobileAuthenticationFilter 里面
        mobileAuthenticationFilter.setAuthenticationManager(
                http.getSharedObject(AuthenticationManager.class));


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

        // 構(gòu)建一個(gè)MobileAuthenticationProvider實(shí)例锋边,接收 mobileUserDetailsService 通過手機(jī)號(hào)查詢用戶信息
        MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
        provider.setUserDetailsService(mobileUserDetailsService);

        // 將provider綁定到 HttpSecurity上懂傀,并將 手機(jī)號(hào)認(rèn)證過濾器綁定到用戶名密碼認(rèn)證過濾器之后
        http.authenticationProvider(provider)
            .addFilterAfter(mobileAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    }
}

MobileAuthenticationFilter 手機(jī)驗(yàn)證filter,完全模仿UsernamePasswordAuthenticationFilter

/**
 * 用于校驗(yàn)用戶手機(jī)號(hào)是否允許通過認(rèn)證
 * 完全復(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);
        //認(rèn)證手機(jī)碼是否正確,通過provider的方式處理,使用哪個(gè)provider,是根據(jù)authRequest是哪個(gè)類型的token
        //這里放的是MobileAuthenticationToken
        return this.getAuthenticationManager().authenticate(authRequest);
    }


    /**
     * 從請(qǐng)求中獲取手機(jī)號(hào)碼
     */
    @Nullable
    protected String obtainMobile(HttpServletRequest request){
        return request.getParameter(mobileParameter);
    }

    /**
     * 從請(qǐng)求中獲取驗(yàn)證碼
     */
    @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請(qǐng)求
     */
    public void setPostOnly(boolean postOnly){
        this.postOnly = postOnly;
    }

    public String getMobileParameter(){
        return mobileParameter;
    }

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

MobileAuthenticationProvider 手機(jī)驗(yàn)證處理器

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

    private UserDetailsService userDetailsService;

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

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

    /**
     * 通過這個(gè)方法,來選擇對(duì)應(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("驗(yàn)證碼不能為空");
        }
        String cacheValidateCode = CacheValidateCode.cacheValidateCodeHashMap.get(mobile);
        if(!inpuCode.equalsIgnoreCase(cacheValidateCode)){
            throw new AuthenticationServiceException("驗(yàn)證碼輸入錯(cuò)誤");
        }
    }

    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 手機(jī)驗(yàn)證碼的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. 生成一個(gè)手機(jī)驗(yàn)證碼
        String code = RandomStringUtils.randomNumeric(4);
        // 2. 將手機(jī)獲取的信息保存到緩存里,實(shí)際應(yīng)用中,可以放到redis中
        String mobile = request.getParameter("mobile");
        CacheValidateCode.cacheValidateCodeHashMap.put(mobile, code);
        System.out.println("手機(jī)驗(yàn)證碼"+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">手機(jī)號(hào):</label>
    <input type="text" name="mobile" id="mobile">

    <label for="sendCode">驗(yàn)證碼:</label>
    <input type="text" name="code" id="sendCode">
    <button type="submit">登錄</button>
</form>
<button onclick="sendCode()"> 獲取驗(yàn)證碼 </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)于手機(jī)的驗(yàn)證filter,并放到security中,在通過驗(yàn)證碼登入的時(shí)候,首先創(chuàng)建MobileAuthenticationToken,遍歷所有的provider的時(shí)候,通過support方法獲取到使用哪個(gè)provider,MobileAuthenticationProvider手機(jī)驗(yàn)證provider,驗(yàn)證手機(jī)號(hào)的驗(yàn)證碼是否正確,如果正確就將MobileAuthenticationToken放到SecurityContextHolder中,保存在ThreadLocal變量中,該線程就能使用了,并且將MobileAuthenticationToken的authenticated設(shè)置為true,在security的最后一個(gè)攔截器FilterSecurityInterceptor判斷是都已經(jīng)驗(yàn)證過了,并且判斷角色是否可以訪問當(dāng)前接口,
這樣就是驗(yàn)證的整個(gè)流程,session的方式驗(yàn)證,在登入成功的時(shí)候token放到tomcat的內(nèi)存中了,key就是sessionid,前端將session傳到server時(shí),從tomcat中獲取已經(jīng)驗(yàn)證過的token,這樣就實(shí)現(xiàn)了登入后,其他接口可以正常訪問的流程.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末茫虽,一起剝皮案震驚了整個(gè)濱河市条获,隨后出現(xiàn)的幾起案子疆栏,更是在濱河造成了極大的恐慌贬媒,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绷旗,死亡現(xiàn)場離奇詭異喜鼓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)衔肢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門庄岖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人膀懈,你說我怎么就攤上這事〗骼” “怎么了启搂?”我有些...
    開封第一講書人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長刘陶。 經(jīng)常有香客問我胳赌,道長,這世上最難降的妖魔是什么匙隔? 我笑而不...
    開封第一講書人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任疑苫,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘捍掺。我一直安慰自己撼短,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開白布挺勿。 她就那樣靜靜地躺著曲横,像睡著了一般。 火紅的嫁衣襯著肌膚如雪不瓶。 梳的紋絲不亂的頭發(fā)上禾嫉,一...
    開封第一講書人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音蚊丐,去河邊找鬼熙参。 笑死,一個(gè)胖子當(dāng)著我的面吹牛麦备,可吹牛的內(nèi)容都是我干的孽椰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼泥兰,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼弄屡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鞋诗,我...
    開封第一講書人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤膀捷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后削彬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體全庸,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年融痛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了壶笼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡雁刷,死狀恐怖覆劈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情沛励,我是刑警寧澤责语,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站目派,受9級(jí)特大地震影響坤候,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜企蹭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一白筹、第九天 我趴在偏房一處隱蔽的房頂上張望智末。 院中可真熱鬧,春花似錦徒河、人聲如沸系馆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽它呀。三九已至,卻和暖如春棒厘,著一層夾襖步出監(jiān)牢的瞬間纵穿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來泰國打工奢人, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谓媒,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓何乎,卻偏偏與公主長得像句惯,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子支救,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容