Springboot security oauth2 jwt實現(xiàn)權(quán)限控制,實現(xiàn)微服務(wù)獲取當(dāng)前用戶信息

為什么使用jwt步鉴?

在原先dubbo+zookeeper項目中,web模塊只暴露Restful接口璃哟,各服務(wù)模塊只暴露duboo接口氛琢,此時用戶登錄后由web項目進(jìn)行token的鑒權(quán)和驗證,并通過dubbo的隱式傳參將sessionID傳遞給dubbo服務(wù)模塊, 攔截器再根據(jù)sessionID從Redis中獲取用戶信息設(shè)置到當(dāng)前線程

然鵝随闪,在springcloud中阳似,各個微服務(wù)直接暴露的是restful接口,此時如何讓各個微服務(wù)獲取到當(dāng)前用戶信息呢铐伴?最佳的方式就是token了撮奏,token作為BS之間的會話標(biāo)識(一般是原生隨機token),同時也可以作為信息的載體傳遞一些自定義信息(jwt当宴, 即Json web token)畜吊。

為了能更清楚的了解本文,需要對spring-security-oauth 及 jwt有一定了解户矢,本文只關(guān)注用戶信息傳遞這一塊

認(rèn)證服務(wù)器

認(rèn)證服務(wù)器配置AuthorizationServerConfigurerAdapter

@Configuration
@PropertySource({"classpath:application.yml"})
@EnableAuthorizationServer
public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    TokenStore tokenStore;

    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    ApprovalStore approvalStore;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .approvalStore(approvalStore)
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允許表單認(rèn)證
        security
                .tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    @Bean
    @Primary
    @ConfigurationProperties("ms-sql.datasource")
    public DataSource dataSource() {
        return new DriverManagerDataSource();
    }

    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource());
    }


    /**
     * 使用 Jwt token
     * @param accessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore(@Autowired JwtAccessTokenConverter accessTokenConverter) {
        return new JwtTokenStore(accessTokenConverter);
    }

    /**
     * token 轉(zhuǎn)換器玲献,加入對稱秘鑰,使用自定tokenEnhancer
     * @return
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
        converter.setSigningKey("secretKey");
        return converter;
    }

}

自定義token轉(zhuǎn)換器
CustomJwtAccessTokenConverter

/**
 * 對JwtAccessTokenConverter 的 enhance進(jìn)行重寫梯浪,加入自定義的信息
 *
 * @author wangqichang
 * @since 2019/4/26
 */
public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {

    private static final String BEARER_PRIFIX = "bearer ";

//這個是token增強器捌年,想讓jwt token攜帶額外的信息在這里處理
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        if (accessToken instanceof DefaultOAuth2AccessToken) {
            Object principal = authentication.getPrincipal();

//這個principal是當(dāng)時登錄后存到securiy的東東,一般是用戶實體驱证,自己debug一下就知道了
            if (principal instanceof OAuthUser) {
                OAuthUser user = (OAuthUser) principal;
                HashMap<String, Object> map = new HashMap<>();

                //jwt默認(rèn)已經(jīng)自帶用戶名延窜,無需再次加入
                map.put("nick_name", user.getUsernickname());
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            }
        }
        return super.enhance(accessToken, authentication);
    }


//主要是資源服務(wù)器解析時一定要有bearer這個頭才認(rèn)為是一個oauth請求,但不知道為啥指定jwt后這個頭就不見了抹锄,特意加上去
    @Override
    protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        return BEARER_PRIFIX + super.encode(accessToken, authentication);
    }
}


此時按照固定格式訪問授權(quán)服務(wù)器token接口獲取token逆瑞,如圖荠藤,可以獲取到j(luò)wt格式的token,并且額外信息nick_name也已經(jīng)添加

image.png

直接解析jwt字符串可以獲取到以下信息获高,即用戶名和授權(quán)信息


image.png

資源服務(wù)器如何鑒權(quán)哈肖?

只需要指定和授權(quán)服務(wù)器一模一樣的token store 和token converter
在securiy的過濾器中OAuth2AuthenticationProcessingFilter會從token中獲取相關(guān)信息進(jìn)行鑒權(quán)
源碼:

 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        boolean debug = logger.isDebugEnabled();
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;

        try {
//這里從請求中獲取到Authorization的 header
            Authentication authentication = this.tokenExtractor.extract(request);
            if (authentication == null) {
                if (this.stateless && this.isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }

                    SecurityContextHolder.clearContext();
                }

                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            } else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                    needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
                }

//這里將調(diào)用當(dāng)前設(shè)定的token Store 和 converter 將字符串token轉(zhuǎn)成Authentication 
                Authentication authResult = this.authenticationManager.authenticate(authentication);
                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                this.eventPublisher.publishAuthenticationSuccess(authResult);
//認(rèn)證成功后,這里將安全上下文設(shè)置用戶信息念秧,一般包含用戶名和權(quán)限淤井。額外信息需要自定義處理,security不會幫你處理的
                SecurityContextHolder.getContext().setAuthentication(authResult);
            }
        } catch (OAuth2Exception var9) {
            SecurityContextHolder.clearContext();
            if (debug) {
                logger.debug("Authentication request failed: " + var9);
            }

            this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
            this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
            return;
        }

        chain.doFilter(request, response);
    }


注意摊趾,資源服務(wù)器主要配置在
ResourceServerConfigurerAdapter

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceConfig extends ResourceServerConfigurerAdapter {


    @Autowired
    TokenStore tokenStore;

    @Autowired
    JwtAccessTokenConverter jwtAccessTokenConverter;


    @Bean
    public TokenStore tokenStore(@Autowired JwtAccessTokenConverter accessTokenConverter) {
        return new JwtTokenStore(accessTokenConverter);
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("secretKey");
        return converter;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        resources.tokenServices(defaultTokenServices);
        super.configure(resources);
    }


//其他資源不做限制币狠,讓security直接放行,這樣只限制注解了的方法
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/**").permitAll();
        super.configure(http);
    }
}


微服務(wù)獲取jwttoken中的用戶信息砾层,兩種方式漩绵,使用security上下文可以直接獲取當(dāng)前用戶名和權(quán)限,另一種自定義攔截器獲取額外信息肛炮。
這個就簡單了止吐,獲取header頭解析驗證token
然后獲取之前從授權(quán)服務(wù)器中的添加的 nick_name的額外信息放入線程變量

/**
 * 用戶信息攔截器
 * 從Header中取出jwttoken,并獲取其中的用戶名設(shè)置到用戶信息上下文線程變量中
 *
 * @author wangqichang
 * @since 2019/4/24
 */
public class UserInfoInterceptor implements HandlerInterceptor {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String authorization = request.getHeader("Authorization");
        authorization = StrUtil.removePrefix(authorization, "bearer ");
        if (StrUtil.isNotBlank(authorization)) {
            Jwt decode = JwtHelper.decode(authorization);
            //驗簽
//            Jwt secretKey = JwtHelper.decodeAndVerify(authorization, new MacSigner("secretKey"));
            String claims = decode.getClaims();
            HashMap<String, Object> hashMap = objectMapper.readValue(claims, HashMap.class);
            Object userName = hashMap.get("user_name");
            Object nickName = hashMap.get("nick_name");
            Object authorities = hashMap.get("authorities");
            UserContext.setUserInfo(UserInfo.builder().userName((String) userName).nickName((String) nickName).build());
        }
        return true;
    }
}

其中用戶上下文類

/**
 * @author wangqichang
 * @since 2019/4/24
 */
public class UserContext {

    private static ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();

    public static UserInfo current() {
        return threadLocal.get();
    }

    public static String currentUserName() {
        UserInfo userInfo = threadLocal.get();
        if (ObjectUtil.isNotNull(userInfo)) {
            return userInfo.getUserName();
        }
        return null;
    }

    public static void setUserInfo(UserInfo userInfo) {
        threadLocal.set(userInfo);
    }
}

啟動攔截器注冊webmvc配置類

/**
 * @author wangqichang
 * @since 2019/4/25
 */
@Configuration
public class CustomWebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 注冊用戶信息攔截器
     *
     * @param registry
     */
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new UserInfoInterceptor()).addPathPatterns("/**");
        super.addInterceptors(registry);
    }

    /**
     * 當(dāng)自定義webmvc配置后侨糟,swagger無法讀取到靜態(tài)Resouce資源碍扔,需要手動添加
     *
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("swagger-ui.html")
                .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/");
        super.addResourceHandlers(registry);
    }
}

在controller中獲取用戶信息如圖

image.png

自定義Oauth異常信息

在默認(rèn)的認(rèn)證異常如圖


image.png

假設(shè)我們做了全局異常處理,前端希望在token過期時做統(tǒng)一的登錄跳轉(zhuǎn)如何做秕重?
實現(xiàn)AuthenticationEntryPoint接口重寫commence方法即可
注意不同,直接拋出異常并不會走@RestControllerAdvice, 因為在這里是response直接返回,并沒有使用到Controller處理

/**
 * @author wangqichang
 * @since 2019/4/30
 */
public class CustomOAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        Response<Object> build = Response.builder().respCode(StatusCode.TOKEN_ERR).respDesc(authException.getMessage()).build();
        response.setContentType("application/json");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        response.getWriter().write(objectMapper.writeValueAsString(build));

    }

    @ConditionalOnMissingBean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}

此時返回我自定義的Response對象溶耘,如圖


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末套鹅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子汰具,更是在濱河造成了極大的恐慌卓鹿,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件留荔,死亡現(xiàn)場離奇詭異吟孙,居然都是意外死亡,警方通過查閱死者的電腦和手機聚蝶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門杰妓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人碘勉,你說我怎么就攤上這事巷挥。” “怎么了验靡?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵倍宾,是天一觀的道長雏节。 經(jīng)常有香客問我,道長高职,這世上最難降的妖魔是什么钩乍? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮怔锌,結(jié)果婚禮上寥粹,老公的妹妹穿的比我還像新娘。我一直安慰自己埃元,他們只是感情好涝涤,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著岛杀,像睡著了一般妄痪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上楞件,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音裳瘪,去河邊找鬼土浸。 笑死,一個胖子當(dāng)著我的面吹牛彭羹,可吹牛的內(nèi)容都是我干的黄伊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼派殷,長吁一口氣:“原來是場噩夢啊……” “哼还最!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起毡惜,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拓轻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后经伙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扶叉,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年帕膜,在試婚紗的時候發(fā)現(xiàn)自己被綠了枣氧。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡垮刹,死狀恐怖达吞,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情荒典,我是刑警寧澤酪劫,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布吞鸭,位于F島的核電站,受9級特大地震影響契耿,放射性物質(zhì)發(fā)生泄漏瞒大。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一搪桂、第九天 我趴在偏房一處隱蔽的房頂上張望透敌。 院中可真熱鬧,春花似錦踢械、人聲如沸酗电。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撵术。三九已至,卻和暖如春话瞧,著一層夾襖步出監(jiān)牢的瞬間嫩与,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工交排, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留划滋,地道東北人。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓埃篓,卻偏偏與公主長得像处坪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子架专,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359