Spring Security OAuth2.0自定義登錄頁面 + JWT Token配置

上一篇文章主要講解Spring Security基本原理,本文主要講如何配置使用Spring Security姑原,包括

  1. OAuth2.0認(rèn)證配置
  2. 自定義登錄頁面實現(xiàn)與配置
  3. JWT token生成配置

基本介紹

pom引用

需要加入spring security和spring security oauth2的依賴

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

OAuth2.0認(rèn)證配置

WebSecurityConfig配置

  1. 首先進(jìn)行登錄頁面相關(guān)配置
  • 配置自定義登錄頁面為index.html悬而,后端用戶名及密碼驗證為"/login"
  • 因為為自定義登錄頁面,配置"/index.html", "/login", "/resources/**", "/static/"不需要認(rèn)證锭汛,其它請求需要認(rèn)證笨奠。否則登錄頁面無法打開
  • 配置退出登錄時袭蝗,刪除session和cookie,并自定義退出成功后的handler
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
    protected void configure(HttpSecurity http) throws Exception {
        // 默認(rèn)支持./login實現(xiàn)authorization_code認(rèn)證
        http
            .formLogin().loginPage("/index.html").loginProcessingUrl("/login")
            .and()
            .authorizeRequests()
            .antMatchers("/index.html", "/login", "/resources/**", "/static/**").permitAll()
            .anyRequest() // 任何請求
            .authenticated()// 都需要身份認(rèn)證
            .and()
            .logout().invalidateHttpSession(true).deleteCookies("JSESSIONID").logoutSuccessHandler(customLogoutSuccessHandler).permitAll()
            .and()
            .csrf().disable();
    }
  1. 加入自定義的UsernamePasswordAuthenticationProvider般婆,用于實現(xiàn)用戶名和密碼認(rèn)證
@Override
    public void configure(AuthenticationManagerBuilder auth) {
        // 定義認(rèn)證的provider用于實現(xiàn)用戶名和密碼認(rèn)證
        auth.authenticationProvider(new UsernamePasswordAuthenticationProvider(usernamePasswordUserDetailService));
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
  1. 實現(xiàn)UsernamePasswordAuthenticationProvider實現(xiàn)authenticate和supports方法用于用戶認(rèn)證
public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    // 自定義實現(xiàn)的user detail到腥,使用了用戶名和密碼驗證,用戶信息的獲取
    private UsernamePasswordUserDetailService userDetailService;

    public UsernamePasswordAuthenticationProvider(UsernamePasswordUserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        // 驗證用戶名和密碼
        if (userDetailService.verifyCredential(username, password)) {

            List<GrantedAuthority> authorities = new ArrayList<>();
            authorities.add(new SimpleGrantedAuthority("user"));
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password, authorities);
            //獲取用戶信息
            token.setDetails(userDetailService.getUserDetail(username));
            return token;
        }
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        // 用戶名和密碼登錄時蔚袍,使用該provider認(rèn)證
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass));
    }
}
  1. 將自定義的登錄頁面編譯后拷貝到resource/public文件下乡范,登錄頁面為index.html∑⊙剩可以自行實現(xiàn)自己版本的登錄頁面晋辆,也可以參考文末awesome-admin中l(wèi)ogin-ui源碼鏈接。


    登錄頁面

JWT Token配置

Oauth2AuthorizationConfig配置

  1. 配置Oauth2 Client宇整,本文通過ClientDetailsService實現(xiàn)Oauth2 Client.
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    ClientDetailsService clientDetailsService;

    ....

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService);
    }
}

ClientDetailsService繼承ClientDetailsService栈拖,正常應(yīng)當(dāng)從數(shù)據(jù)庫中讀取,為了演示方便没陡,本文直接在代碼中設(shè)置∷魃停客戶端信息包括appId, appSecret, grantTypes, accessExpireTime, refreshExpireTime, redirectUrl, scopes等OAuth2協(xié)議中Client信息盼玄。

@Service
@Primary
public class ClientDetailsServiceImpl implements ClientDetailsService {
    @Override
    public ClientDetails loadClientByClientId(String s) {

        if ("weixin".equals(s) || "app".equals(s) || "mini_app".equals(s) || "global".equals(s)) {
            AppCredential credential = new AppCredential();
            credential.setAppId(s);
            credential.setAppSecret("testpassword");
            credential.setGrantTypes("authorization_code,client_credentials,password,refresh_token,mini_app");
            credential.setAccessExpireTime(3600 * 24);
            credential.setRefreshExpireTime(3600 * 24 * 30);
            credential.setRedirectUrl("http://localhost:3006,http://localhost:3006/auth,http://localhost:8000,http://localhost:8000/auth");
            credential.setScopes("all");
            return new AppCredentialDetail(credential);
        }
        return null;
    }
}
  1. 配置JWT Token服務(wù),用于生成token潜腻。配置性的內(nèi)容埃儿,生產(chǎn)環(huán)境從來沒變過,建議直接拷貝
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
    ...
    @Autowired
    TokenServiceFactory tokenServiceFactory;

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .tokenServices(tokenServiceFactory.JwtTokenService())
                .authenticationManager(authenticationManager);
    }
}

本文通過實現(xiàn)TokenServiceFactory實現(xiàn)Token Service融涣,用于生成token童番,這部分代碼比較瑣碎,建議直接運(yùn)用copy&paste技術(shù)(因為生產(chǎn)使用一年了威鹿,就沒動過)剃斧。核心注意3點:

  1. 實現(xiàn)TokenEnhancer用于在token中加入一些自定義的信息
  2. 實現(xiàn)TokenConverter用于將哪些字段放入token
  3. 在accessTokenConverter方法中設(shè)置token生成的公鑰和密鑰
@Service
public class TokenServiceFactory {

    private TokenKeyConfig tokenKeyConfig;
    private ClientDetailsService clientDetailsService;

    @Autowired
    public TokenServiceFactory(
            TokenKeyConfig tokenKeyConfig,
            ClientDetailsService clientDetailsService) {
        this.tokenKeyConfig = tokenKeyConfig;
        this.clientDetailsService = clientDetailsService;
    }

    @Bean
    @Primary
    public AuthorizationServerTokenServices JwtTokenService() {
        final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        // 設(shè)置自定義的TokenEnhancer, TokenConverter,用于在token中增加自定義的內(nèi)容
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));

        return defaultTokenService(tokenEnhancerChain);
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setAccessTokenConverter(new CustomAccessTokenConverter());
        // 設(shè)置token生成的公鑰和密鑰忽你,密鑰放在resource目錄下
        final KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(
                new ClassPathResource(tokenKeyConfig.getPath()), tokenKeyConfig.getPassword().toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair(tokenKeyConfig.getAlias()));

        return converter;
    }

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

    @Bean
    public org.springframework.security.oauth2.provider.token.TokenEnhancer tokenEnhancer() {
        return new TokenEnhancer();
    }

    private AuthorizationServerTokenServices defaultTokenService(TokenEnhancerChain tokenEnhancerChain) {
        final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
        defaultTokenServices.setClientDetailsService(clientDetailsService);
        return defaultTokenServices;
    }
}

密鑰生成參考:http://www.reibang.com/p/c9d5a2aa8648
TokenEnhancer用于在token中加入自定義的內(nèi)容幼东,通常需要自己實現(xiàn),會經(jīng)常隨著需求變動修改

public class TokenEnhancer implements org.springframework.security.oauth2.provider.token.TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        final Map<String, Object> additionalInfo = new HashMap<>(2);

        Authentication userAuthentication = authentication.getUserAuthentication();
        // 如果是client認(rèn)證科雳,通常是服務(wù)間調(diào)取認(rèn)證根蟹,token中加入admin角色
        if (authentication.isClientOnly()) {
            List<String> authorities = new ArrayList<>(1);
            authorities.add("admin");
            additionalInfo.put("authorities", authorities);

        } else {
           // 如果是用戶認(rèn)證,token中加入user detail糟秘,驗證用戶名和密碼時設(shè)置的user detail
            additionalInfo.put("userInfo", userAuthentication.getDetails());

            // 將authorities轉(zhuǎn)換為string類型简逮,便于json序列化
            Set<GrantedAuthority> rolesInfo = new HashSet<>(userAuthentication.getAuthorities());
            additionalInfo.put("authorities", rolesInfo.stream().map(auth -> auth.getAuthority()).toArray());
        }

        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return accessToken;
    }
}

CustomAccessTokenConverter通常是把所有claims放到token 中

@Component
public class CustomAccessTokenConverter extends DefaultAccessTokenConverter {

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
        OAuth2Authentication authentication = super.extractAuthentication(claims);
        authentication.setDetails(claims);
        return authentication;
    }
}

ResourceServerConfig配置

Spring Security除了作為認(rèn)證服務(wù)器,本身還是資源服務(wù)器尿赚。本項目Spring Security支持自定義的登陸頁面還有一些API接口散庶,因此需要配置ResourceServerConfig

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

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

        http.requestMatchers().antMatchers("/api/**", "/users/**", "/static/**")
                .and()
                .authorizeRequests()
                .antMatchers("/api/**", "/users/**").authenticated();
    }
}

Application注解

@SpringBootApplication
@EnableResourceServer                //作為resource server
@EnableGlobalMethodSecurity(prePostEnabled = true)    //允許在方法前加注解確定權(quán)限@PreAuthorize("hasAnyAuthority('admin',)")
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
    }
}

效果

輸入登錄地址蕉堰,因為涉及到前后端的配合及通過code換取token的轉(zhuǎn)換,本項目實現(xiàn)了一個前端項目直接使用Spring Security及其登錄頁面督赤,登錄成功后跳回到前端首頁嘁灯,參考文末源碼admin-ui。

  1. 登錄頁面輸入localhost:8000然后會跳轉(zhuǎn)到auth服務(wù)的登錄頁面如下圖,用戶名可以輸入admin/admin實際上可以隨便輸入躲舌,因為上面認(rèn)證代碼實質(zhì)上沒有檢驗密碼


    登錄頁面
  1. 登錄后成功頁面丑婿, JWT Token保存在cookie中。


    2.png

如果想直接訪問Spring Security中的登錄頁面没卸,則啟動你的Spring Security服務(wù)訪問下面連接(假設(shè)服務(wù)端口為5000)羹奉,也可以下載文末awesome-admin中的admin-service源碼啟動config、registry约计、auth服務(wù)诀拭。登錄后可以成功跳轉(zhuǎn)到redirect_uri并且生成了code,用此code可以通過postman獲取token煤蚌。

  1. 使用下面登錄url, 輸入用戶名和密碼均為admin后跳轉(zhuǎn)的redirect_uri
    http://localhost:5000/auth/oauth/authorize?response_type=code&client_id=app&redirect_uri=http://localhost:3006/auth

  2. 跳轉(zhuǎn)后可以看到連接中的code


    登錄成功后獲取code
  3. 以code為參數(shù)通過API獲取token耕挨,注意要使用basic auth認(rèn)證,用戶名和密碼就是你之前在 ClientDetailsService中配置的app和appsecret
    http://localhost:5000/auth/oauth/token?grant_type=authorization_code&code=pYIspF&redirect_uri=http://localhost:3006/auth

    Code獲取JWT Token API

面向Copy&Paste編程

  1. awesome-admin源碼
    https://gitee.com/awesome-engineer/awesome-admin
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尉桩,一起剝皮案震驚了整個濱河市筒占,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜘犁,老刑警劉巖翰苫,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異这橙,居然都是意外死亡奏窑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門屈扎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埃唯,“玉大人,你說我怎么就攤上這事鹰晨≈欤” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵并村,是天一觀的道長巍实。 經(jīng)常有香客問我,道長哩牍,這世上最難降的妖魔是什么棚潦? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮膝昆,結(jié)果婚禮上丸边,老公的妹妹穿的比我還像新娘叠必。我一直安慰自己,他們只是感情好妹窖,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布纬朝。 她就那樣靜靜地躺著,像睡著了一般骄呼。 火紅的嫁衣襯著肌膚如雪共苛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天蜓萄,我揣著相機(jī)與錄音隅茎,去河邊找鬼。 笑死嫉沽,一個胖子當(dāng)著我的面吹牛辟犀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播绸硕,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼堂竟,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了玻佩?” 一聲冷哼從身側(cè)響起跃捣,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翼悴,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡全度,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了闻镶。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片甚脉。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖铆农,靈堂內(nèi)的尸體忽然破棺而出牺氨,到底是詐尸還是另有隱情,我是刑警寧澤墩剖,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布猴凹,位于F島的核電站,受9級特大地震影響岭皂,放射性物質(zhì)發(fā)生泄漏郊霎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一爷绘、第九天 我趴在偏房一處隱蔽的房頂上張望书劝。 院中可真熱鬧进倍,春花似錦、人聲如沸购对。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽骡苞。三九已至垂蜗,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間烙如,已是汗流浹背么抗。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留亚铁,地道東北人蝇刀。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像徘溢,于是被迫代替她去往敵國和親吞琐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345