數據的建設可以去看 我之前的博客
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
