oauth2+spring security +jwt 完成分布式服務認證


數據的建設可以去看 我之前的博客

package com.aila.config;

import com.aila.utils.UserJwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

/**
 * @Author: {---chenzhichao---}
 * @Date: 2020/6/5 11:23
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    ClientDetailsService clientDetailsService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //取出身份,如果身份為空說明沒有認證
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //沒有認證統一采用httpbasic認證,httpbasic中存儲了client_id和client_secret,開始認證client_id和client_secret
        if(authentication==null){
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(username);
            if(clientDetails!=null){
                //秘鑰
                String clientSecret = clientDetails.getClientSecret();
                //靜態方式
                //return new User(username,new BCryptPasswordEncoder().encode(clientSecret), AuthorityUtils.commaSeparatedStringToAuthorityList(""));
                //數據庫查找方式
                return new User(username,clientSecret, AuthorityUtils.commaSeparatedStringToAuthorityList(""));
            }
        }

        if (StringUtils.isEmpty(username)) {
            return null;
        }
        BCryptPasswordEncoder passwordEncoder=new BCryptPasswordEncoder();
        String encode = passwordEncoder.encode("qpalzm");
        String permissions = "salesman,accountant,user";
        UserJwt jwt = new UserJwt(username, encode, AuthorityUtils.commaSeparatedStringToAuthorityList(permissions));
        return jwt;
    }
}

這里對使用了BCrypt 加密方式 這里主要對數據庫查詢用戶信息   把用戶的密碼和權限查出來 返回給oauth2 進行校驗

如果登錄成功 應該返回如下

{
    "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MTU5MTU1NDcwMSwiYXV0aG9yaXRpZXMiOlsiYWNjb3VudGFudCIsInVzZXIiLCJzYWxlc21hbiJdLCJqdGkiOiI4YmNkNzRmYS0zMWFkLTRjZDktOTJhZi05OThjMmEwMWM4NDgiLCJjbGllbnRfaWQiOiJhaWxhIiwidXNlcm5hbWUiOiJjemMifQ.cXGPAZlhCLUgI44fXHejOXjdXA8YNaQhLOLGvkSrclr4clcUqy9AKMuzW9L5ssNA_q9lCH2IuX8uAJI9WNDS0Opx4EQ54YwRE-uH4QgfoMlz_4vyAcTWuSF6OTgjxRmTSX1oXwkCr70_l0_9rrkXzGoorAECkbEPA5D_t27gDRVTI0biQ8l87PlNV3qt86c1y6X2b4vKuV16I29PyKjCBAUb9acQRehiwHPtF53gtJ_MKkh7eA0pugfK26M0KtC9t93bRVpEd1vuahVuhxPSvnsQRK5LSwml0FcW7I7CWF8GVSIHDE3VtYKfS1mTjxwoeRLOlE0GAAd_ZXDoD33WzA",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJhcHAiXSwiYXRpIjoiOGJjZDc0ZmEtMzFhZC00Y2Q5LTkyYWYtOTk4YzJhMDFjODQ4IiwibmFtZSI6bnVsbCwiaWQiOm51bGwsImV4cCI6MTU5MTU1NDcwMSwiYXV0aG9yaXRpZXMiOlsiYWNjb3VudGFudCIsInVzZXIiLCJzYWxlc21hbiJdLCJqdGkiOiJmNzkzMDg5Yi0xYjk1LTRiZmMtOTIwMy1iZjgwMjJjZWYwNGQiLCJjbGllbnRfaWQiOiJhaWxhIiwidXNlcm5hbWUiOiJjemMifQ.MbaFrxhg6C5z_oL1LJZsxV6HCEjE4BeYDGKHIiMwJ0hYAfl1Ad6q2bRRE_J_Jd5ByovHF_uzJTyRHjgSomNUqpJqkfcLFEFlAg-BTYbCB_19npGInMDqCqyVsUwgiza04rflONrwUxgcHtMwUJTxIe1JS5jFEsaHry55o3Gr_zQBMyg3bp4MEDtjgozBLkwq42LXDu1E3wtEOVt3jSiQaz0_Zf96P4Dj2T6t0wigRi4GUUSWyzh_V4qM1e6u3jBZC49C1oJ8la11XYLZnF03PNV1g_OGlD44zjVRfz7swBnko2A_xMxZPbQnmCgxPaX6nuev2-SUFPg2OkP6tkq38A",
    "expires_in": 43199,
    "scope": "app",
    "jti": "8bcd74fa-31ad-4cd9-92af-998c2a01c848"
}

當然你也可以讓前端解放雙手不需要每次請求一個服務都需要在請求的頭部封裝jwt令牌

將jti(jwt的唯一短標識)放在用戶的cookie 中 講jwt令牌作為 string類型的values key為jti 存放在redis 中設置一個過期時間

這樣下次訪問微服務只需要在網關里去判斷 cookie中的jti 在redis中有沒有存在就可以判斷用戶有沒有登錄  將jwt令牌手動封裝在請求的頭部 轉發給具體的微服務

package com.aila.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bootstrap.encrypt.KeyProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.security.KeyPair;


@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    //數據源,用於從數據庫獲取數據進行認證操作,測試可以從內存中獲取
    @Autowired
    private DataSource dataSource;
    //jwt令牌轉換器
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    //SpringSecurity 用戶自定義授權認證類
    @Autowired
    UserDetailsService userDetailsService;
    //授權認證管理器
    @Autowired
    AuthenticationManager authenticationManager;
    //令牌持久化存儲接口
    @Autowired
    TokenStore tokenStore;
    @Autowired
    private CustomUserAuthenticationConverter customUserAuthenticationConverter;

    /***
     * 客戶端信息配置
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource).clients(clientDetails());
    }

    /***
     * 授權服務器端點配置
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        tokenServices.setAccessTokenValiditySeconds(60*60*2);//token有效期設置2個小時
        tokenServices.setRefreshTokenValiditySeconds(60*60*12);//Refresh_token:12個小時

        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                .authenticationManager(authenticationManager)//認證管理器
                .tokenStore(tokenStore)                       //令牌存儲
                .userDetailsService(userDetailsService) //用戶信息service
                /*.tokenServices(tokenServices)*/;
    }

    /***
     * 授權服務器的安全配置
     * @param oauthServer
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer.allowFormAuthenticationForClients()
                .passwordEncoder(new BCryptPasswordEncoder())
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                ;
    }


    //讀取密鑰的配置
    @Bean("keyProp")
    public KeyProperties keyProperties(){
        return new KeyProperties();
    }

    @Resource(name = "keyProp")
    private KeyProperties keyProperties;

    //客戶端配置
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

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

    /****
     * JWT令牌轉換器
     * @param customUserAuthenticationConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyPair keyPair = new KeyStoreKeyFactory(
                keyProperties.getKeyStore().getLocation(),                          //證書路徑 aila.jks
                keyProperties.getKeyStore().getSecret().toCharArray())              //證書秘鑰 ailapass
                .getKeyPair(
                        keyProperties.getKeyStore().getAlias(),                     //證書別名 aila
                        keyProperties.getKeyStore().getPassword().toCharArray());   //證書密碼 ailapass
        converter.setKeyPair(keyPair);
        //配置自定義的CustomUserAuthenticationConverter
        DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter();
        accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter);
        return converter;
    }
}
package com.aila.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
@Order(-1)
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /***
     * 忽略安全攔截的URL
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/oauth/login",
                "/oauth/logout","/oauth/toLogin","/login.html","/css/**","/data/**","/fonts/**","/img/**","/js/**");
    }

    /***
     * 創建授權管理認證對象
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    /***
     * 采用BCryptPasswordEncoder對密碼進行編碼
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /****
     *
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .httpBasic()        //啟用Http基本身份驗證
                .and()
                .formLogin()       //啟用表單身份驗證
                .and()
                .authorizeRequests()    //限制基於Request請求訪問
                .anyRequest()
                .authenticated();       //其他請求都需要經過驗證

        //開啟表單登錄
        http.formLogin().loginPage("/oauth/toLogin")//設置訪問登錄頁面的路徑
                .loginProcessingUrl("/oauth/login");//設置執行登錄操作的路徑
    }
}
server:
  port: 9200
spring:
  application:
    name: auth2
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/class19?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC
    username: root
    password: root
auth:
  ttl: 3600  #token存儲到redis的過期時間
  clientId: aila
  clientSecret: aila
  cookieDomain: localhost
  cookieMaxAge: -1
encrypt:
  key-store:
    location: classpath:/aila.jks
    secret: ailapass
    alias: aila
    password: ailapass


這些是oauth2 的配置

 

接下來是需要jwt令牌的微服務配置

package com.aila.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.stream.Collectors;

@Configuration
@EnableResourceServer
//開啟方法上的PreAuthorize注解
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //公鑰
    private static final String PUBLIC_KEY = "public.key";

    /***
     * 定義JwtTokenStore
     * @param jwtAccessTokenConverter
     * @return
     */
    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    /***
     * 定義JJwtAccessTokenConverter
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(getPubKey());
        return converter;
    }
    /**
     * 獲取非對稱加密公鑰 Key
     * @return 公鑰 Key
     */
    private String getPubKey() {
        Resource resource = new ClassPathResource(PUBLIC_KEY);
        try {
            InputStreamReader inputStreamReader = new InputStreamReader(resource.getInputStream());
            BufferedReader br = new BufferedReader(inputStreamReader);
            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            return null;
        }
    }

    /***
     * Http安全配置,對每個到達系統的http請求鏈接進行校驗
     * @param http
     * @throws Exception
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //所有請求必須認證通過
        http.authorizeRequests()
                .anyRequest().
                authenticated();    //其他地址需要認證授權
    }
}

jwttoken 會去解析封裝太頭部的 jwt令牌 對他進行解密操作 如果不能解密說明 請求不可靠拒絕訪問

{
    "error": "invalid_token",
    "error_description": "Cannot convert access token to JSON"
}



401錯誤

既然是security 那就可以做權限校驗,在oauth中 我們已經封裝了三個權限 user,salesmen,accountant

在c層的方法上@PreAuthorize("hasAuthority('admin')") 這樣 必須要有admin 權限才能訪問  ,請求結果

{
    "error": "access_denied",
    "error_description": "不允許訪問"
}

錯誤代碼403

公鑰和私鑰的生成可以去百度  需要用到OpenSSL


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM