前后端分離項(xiàng)目整合spring security5

spring security的處理流程

spring security采用一系列鏈?zhǔn)讲僮鱽硗瓿烧J(rèn)證和鑒權(quán)任務(wù)笋妥。它的總體流程在江南一點(diǎn)雨的博客中有寫辱匿,這里就不在羅列了。這里僅對(duì)我們需要自定義的部分進(jìn)行提取挽牢。

Spring Security

登錄處理

Spring Security 默認(rèn)使用form表單進(jìn)行登錄,在基本的無配置情況下他會(huì)在使用自帶的登錄頁面,并且在控制臺(tái)打印出一串密碼以進(jìn)行登錄驗(yàn)證喷户。而這顯然不符合實(shí)際的情況。所以访锻,Spring Security提供基本的配置類來進(jìn)行設(shè)置褪尝。只要在配置類中繼承WebSecurityConfigurerAdapter就可以對(duì)Spring Security進(jìn)行簡單且高效的配置闹获。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().cors()
                .and()
                .formLogin()
                .loginPage("/login")
                .usernameParameter("loginName")
                .passwordParameter("passwd")
                .successForwardUrl("/index")
                .failureForwardUrl("/error");
    }
}

登錄過程實(shí)現(xiàn)

只要使用.loginPage()就可以指定自己編寫的登錄界面了。但是需要注意的是默認(rèn)情況下表單中的參數(shù)名必須得是usernamepassword河哑。如果參數(shù)名不一致的話避诽,spring security是無法進(jìn)行匹配的。當(dāng)然你也可以使用.usernameParameter.passwordParameter來指定前端傳來的from表單內(nèi)容璃谨。除此之外沙庐,我們還需要實(shí)現(xiàn)一個(gè)UserDetailService接口來從數(shù)據(jù)庫或內(nèi)存中獲取正確的User信息用以判斷登錄成功與否。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = userService.findByLoginName(username);

        if (sysUser == null) {
            throw new UsernameNotFoundException("用戶名或密碼不正確");
        }
        return new UserDetails("具體數(shù)據(jù)");
    }

}

該接口只有一個(gè)loadUserByUsername方法需要實(shí)現(xiàn)該方法需要返回一個(gè)UserDetail類用以存放真實(shí)的用戶信息佳吞,這個(gè)UserDetail也是一個(gè)接口拱雏。
源碼如下:

public interface UserDetails extends Serializable {

    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

spring security也提供實(shí)現(xiàn)類User,一般情況下拿來即用就行底扳,也可以通過繼承進(jìn)行拓展铸抑。具體根據(jù)實(shí)際的業(yè)務(wù)邏輯來決定。最后在配置類中將寫好的UserDetailService載入AuthenticationManager即可花盐。這樣就能將登錄過程完全托管給spring security來處理了羡滑。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);
    }

登錄成功和登錄失敗處理

但是很遺憾啊,公司里的項(xiàng)目是前后端分離式的算芯,所以上面的配置并不能滿足我們的需求柒昏。前后端分離項(xiàng)目的特點(diǎn)就是前后端之間只是用json數(shù)據(jù)進(jìn)行傳輸,頁面路由完全由前端控制熙揍。所以合理解決方案是职祷,在登錄成功后前端發(fā)送一個(gè)攜帶憑證的json數(shù)據(jù),登錄失敗的話也返回對(duì)應(yīng)的json數(shù)據(jù)届囚。
根據(jù)上面的流程圖可以看出有梆,在登錄驗(yàn)證之后,SpringSecurity 會(huì)根據(jù)驗(yàn)證結(jié)果意系,進(jìn)入AuthenticationFailureHandlerAuthenticationSuccessHandler中泥耀,所以只要我們實(shí)現(xiàn)這兩個(gè)接口,就可以對(duì)登錄處理進(jìn)行自定義了蛔添。
登錄失敗的話我們將失敗原因封裝成固定的json格式返回回去即可痰催。

@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        OutputStream out = response.getOutputStream();
        String result;
        ObjectMapper mapper = new ObjectMapper();

        if (exception instanceof BadCredentialsException) {
            result = mapper.writeValueAsString(BaseResponse.fail("用戶名或者密碼錯(cuò)誤"));
        } else if (exception instanceof DisabledException) {
            result = mapper.writeValueAsString(BaseResponse.fail("該賬戶已禁用"));
        } else {
            result = mapper.writeValueAsString(BaseResponse.fail(exception.getMessage()));
        }


        out.write(result.getBytes(StandardCharsets.UTF_8));
        out.flush();
        out.close();
    }
}

登錄成功的邏輯也差不多,生成一個(gè)特定的憑證返回給前端使用就好迎瞧,這里我們采用jwt(json web token)作為憑證夸溶。

@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json; charset=UTF-8");
        ServletOutputStream out = response.getOutputStream();

        out.write(getResponse(authentication).getBytes(StandardCharsets.UTF_8));

        out.flush();
        out.close();
    }

    private String getResponse(Authentication authentication) throws JsonProcessingException {
//        獲取spring security user對(duì)象
        User user = (User) authentication.getPrincipal();
//        用jwt工具類根據(jù)用戶信息來生成token
        String token = JwtUtil.sign(user);
//        將spring security user轉(zhuǎn)化成vo傳回去
        LoginUserVo lv = new LoginVo(user,token);
        BaseResponse<LoginUserVo> result = BaseResponse.success(lv);

        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(result);
    }
}

最后別忘記將兩個(gè)自定義的實(shí)現(xiàn)類編入spring security中去

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().cors()
                .and()
//      登錄驗(yàn)證邏輯
                .formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/login")
                .usernameParameter("loginName")
                .passwordParameter("passwd")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler);

登出處理

因?yàn)椴捎玫膉wt機(jī)制的無狀態(tài)服務(wù)凶硅,而且jwt是無法手動(dòng)注銷的缝裁,所以其實(shí)登出操作只要在前端把token從緩存中刪除就可以了。不過由于我們采用了無狀態(tài)服務(wù)足绅,所以還要配置將session給關(guān)閉

                http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

雖然session和jwt相互并不沖突捷绑,但是為了效率考量還是關(guān)閉session比較合理韩脑,不然服務(wù)端會(huì)積累太多session id的。
當(dāng)然如果是用了redis等緩存機(jī)制胎食,還要在登出成功后在緩存中也將token進(jìn)行同步刪除扰才。這一點(diǎn)可以通過實(shí)現(xiàn)LogoutSuccessHandler來實(shí)現(xiàn),詳見上述流程圖厕怜。

認(rèn)證驗(yàn)證

將整個(gè)登錄登出模塊配置完成之后衩匣,就需要解決如何驗(yàn)證已登錄問題,而spring security自生自然使用session id 來進(jìn)行驗(yàn)證的粥航,但是之前我們已經(jīng)將session給關(guān)閉了琅捏。所以就要用到我們自己派發(fā)的jwt了,只要一個(gè)請(qǐng)求的請(qǐng)求頭中攜帶了我們的jwt递雀,且jwt未過期柄延,我們就認(rèn)為他是已經(jīng)登錄。
而要實(shí)現(xiàn)這個(gè)需求缀程,自然是又要自定義一個(gè)過濾器了搜吧。這次我們采用繼承BasicauthenticationFilter 且重寫doFilterInternal 的方式來實(shí)現(xiàn)。

public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String token = request.getHeader("Authorization");

        if (JwtUtil.verify(token)) {

            UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken("username", null,
                            "權(quán)限列表");

//            將token中的信息放到security context中用以后續(xù)驗(yàn)證
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
        chain.doFilter(request, response);
    }
}

這里的邏輯也相當(dāng)簡單杨凑,從前端的請(qǐng)求頭中獲取jwt判斷是否有效滤奈,如果有效則在spring security context中設(shè)置相應(yīng)的權(quán)限,否則就直接交給之后的過濾器來驗(yàn)證撩满。唯一需要注意的就是這個(gè)UsernamePasswordAuthenticationToken這是spring security中authentication 的一個(gè)具體實(shí)現(xiàn)蜒程。可以從中通過getPrncipal來獲取當(dāng)前的用戶信息伺帘,通過getCredentials來獲取當(dāng)前用戶的密碼昭躺,通過getDetails來獲取請(qǐng)求的更多詳細(xì)信息。然后因?yàn)槲覀兪鞘褂胘wt作為憑證的伪嫁,自然不可能在里面存放密碼领炫,所以就將密碼設(shè)為null了。值得一提的是张咳,最后一項(xiàng)權(quán)限列表是必填的不能為null驹吮。即使你的業(yè)務(wù)邏輯里沒有權(quán)限認(rèn)證,你也需要提供一個(gè)權(quán)限作為認(rèn)證權(quán)限晶伦,不然即使已登錄也是無法訪問到任何controller的。
最后將這個(gè)過濾器在配置類中進(jìn)行配置啄枕。

http.addFilter(getJwtAuthenticationFilter());

添加過濾器一共有四種方式addFilterAt,addFilterBefore,addFilterAfter,addFilter婚陪。基本上可以做到見名知義频祝,而他生效原理么泌参,就和spring security中對(duì)過濾的實(shí)現(xiàn)方式有關(guān)了脆淹。spring security會(huì)維護(hù)一個(gè)filter序列,并通過優(yōu)先級(jí)來判斷當(dāng)前應(yīng)該執(zhí)行哪個(gè)過濾器沽一,spring security對(duì)自己實(shí)現(xiàn)過濾器已經(jīng)有了默認(rèn)的優(yōu)先級(jí)配置盖溺,所以前三個(gè)方法分別可以獲取目標(biāo)過濾器的優(yōu)先級(jí),優(yōu)先級(jí)+1铣缠,優(yōu)先級(jí)-1烘嘱。如果兩個(gè)過濾器的優(yōu)先級(jí)相同的話會(huì)優(yōu)先執(zhí)行我們自定義的過濾器。詳見原文總結(jié)的相當(dāng)全面了蝗蛙。然后就是我們這里用到的addFilter了,使用這個(gè)方法的話spring security會(huì)去判斷這個(gè)過濾器是否已經(jīng)注冊(cè)到filter列表中蝇庭,如果沒有就會(huì)報(bào)沒有指定優(yōu)先級(jí)錯(cuò)

public HttpSecurity addFilter(Filter filter) {
        Class<? extends Filter> filterClass = filter.getClass();
        if (!comparator.isRegistered(filterClass)) {
            throw new IllegalArgumentException(
                    "The Filter class "
                            + filterClass.getName()
                            + " does not have a registered order and cannot be added without a specified order. Consider using addFilterBefore or addFilterAfter instead.");
        }
        this.filters.add(filter);
        return this;
    }
    public boolean isRegistered(Class<? extends Filter> filter) {
        return getOrder(filter) != null;
    }

很顯然我們自定義的過濾器沒有被注冊(cè),但是為啥沒有報(bào)錯(cuò)呢捡硅,那肯定能想到是因?yàn)槔^承了父類的優(yōu)先級(jí)哮内,而事實(shí)也是如此:

    private Integer getOrder(Class<?> clazz) {
        while (clazz != null) {
            Integer result = filterToOrder.get(clazz.getName());
            if (result != null) {
                return result;
            }
            clazz = clazz.getSuperclass();
        }
        return null;
    }

認(rèn)證和權(quán)限異常處理

在完成了登錄驗(yàn)證之后,自然是要對(duì)未登錄的請(qǐng)求進(jìn)行攔截和向前端發(fā)送對(duì)應(yīng)信息了壮韭。攔截這一部分spring security可以幫我們做北发,但是前后端分離狀態(tài)的消息回送自然要我們來手動(dòng)處理。參考上面的流程圖可以得知喷屋,spring security的異常處理有兩個(gè)入口琳拨,一是認(rèn)證異常處理AuthenticationEntryPoint,一是權(quán)限不足異常處理AccessDeniedHandler。所以只要實(shí)現(xiàn)這個(gè)兩個(gè)接口并在配置類中進(jìn)行處理就行了逼蒙。

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        OutputStream out = response.getOutputStream();

        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(401);
        String result = mapper.writeValueAsString(BaseResponse.fail("用戶未認(rèn)證"));

        out.write(result.getBytes(StandardCharsets.UTF_8));
        out.flush();
        out.close();
    }
}
@Component
public class JwtDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        OutputStream out = response.getOutputStream();

        ObjectMapper mapper = new ObjectMapper();
        response.setStatus(403);
        String result;
        if (accessDeniedException instanceof AuthorizationServiceException) {
            result = mapper.writeValueAsString(BaseResponse.fail("無訪問權(quán)限"));
        } else {
            result = mapper.writeValueAsString(BaseResponse.fail(accessDeniedException.getMessage()));
        }

        out.write(result.getBytes(StandardCharsets.UTF_8));
        out.flush();
        out.close();
    }
}

關(guān)鍵詞

springboot从绘, spring security, 前后端分離是牢, jwt僵井, restful


純小白,寫的有點(diǎn)亂驳棱,如有問題還請(qǐng)指正orz批什,會(huì)不斷完善內(nèi)容的。
參考自江南一點(diǎn)雨大神的博客社搅。原文指路

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驻债,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子形葬,更是在濱河造成了極大的恐慌合呐,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件笙以,死亡現(xiàn)場離奇詭異淌实,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門拆祈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恨闪,“玉大人,你說我怎么就攤上這事放坏×剩” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵淤年,是天一觀的道長钧敞。 經(jīng)常有香客問我,道長互亮,這世上最難降的妖魔是什么犁享? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮豹休,結(jié)果婚禮上炊昆,老公的妹妹穿的比我還像新娘。我一直安慰自己威根,他們只是感情好凤巨,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著洛搀,像睡著了一般尔艇。 火紅的嫁衣襯著肌膚如雪贤旷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音辉巡,去河邊找鬼拴驮。 笑死楞陷,一個(gè)胖子當(dāng)著我的面吹牛韵洋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播景图,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼较雕,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了挚币?” 一聲冷哼從身側(cè)響起亮蒋,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎妆毕,沒想到半個(gè)月后慎玖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡笛粘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年凄吏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了远舅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡痕钢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出序六,到底是詐尸還是另有隱情任连,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布例诀,位于F島的核電站随抠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏繁涂。R本人自食惡果不足惜拱她,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望扔罪。 院中可真熱鬧秉沼,春花似錦、人聲如沸矿酵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽全肮。三九已至敞咧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間辜腺,已是汗流浹背休建。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留评疗,地道東北人测砂。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像壤巷,于是被迫代替她去往敵國和親邑彪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

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