spingsecurity+oauth2+jwt實現sso
前提
1、在閱讀此文時你應該有對oauth2的基本了解,及jwt的組成及springsecurity的基本配置。
2、使用RSA生成jwt及驗證
1.1 生成公鑰和和私鑰
(1)keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore
Keytool 是一個java提供的證書管理工具
-alias:密鑰的別名
-keyalg:使用的hash算法
-keypass:密鑰的訪問密碼
-keystore:密鑰庫文件名,xc.keystore保存了生成的證書
-storepass:密鑰庫的訪問密碼
這里有個小坑,新版本的keytool 不支持 設置密鑰的訪問密碼,我們在獲取秘鑰對時也不用去指定密碼
(2)導出公鑰
去這個網址http://slproweb.com/products/Win32OpenSSL.html 下載 Win64 OpenSSL v1.1.1h Light安裝后將其配置到環境變量中然后執行如下命令
keytool -list -rfc --keystore xc.keystore | openssl x509 -inform pem -pubkey
然后將導出的公鑰設為一行存為.txt文件
(3)將生成的證書文件和公鑰文件放在resource目錄下,使用如下代碼來測試生成jwt及驗證jwt
import com.alibaba.fastjson.JSON; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.core.io.ClassPathResource; import org.springframework.security.jwt.Jwt; import org.springframework.security.jwt.JwtHelper; import org.springframework.security.jwt.crypto.sign.RsaSigner; import org.springframework.security.jwt.crypto.sign.RsaVerifier; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import org.springframework.test.context.junit4.SpringRunner; import java.net.URL; import java.security.KeyPair; import java.security.interfaces.RSAPrivateKey; import java.util.HashMap; import java.util.Map; /** * @author Administrator * @version 1.0 **/ public class TestJwt { //創建jwt令牌 @Test public void testCreateJwt(){ //密鑰庫文件 String keystore = "xc.keystore"; //密鑰庫的密碼 String keystore_password = "xuechengkeystore"; //密鑰庫文件路徑 ClassPathResource classPathResource = new ClassPathResource(keystore); //密鑰別名 String alias = "xckey";//密鑰工廠 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(classPathResource,keystore_password.toCharArray()); //密鑰對(公鑰和私鑰) KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias); //獲取私鑰 RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate(); //jwt令牌的內容 Map<String,String> body = new HashMap<>(); body.put("name","itcast"); String bodyString = JSON.toJSONString(body); //生成jwt令牌 Jwt jwt = JwtHelper.encode(bodyString, new RsaSigner(aPrivate)); //生成jwt令牌編碼 String encoded = jwt.getEncoded(); System.out.println(encoded); } //校驗jwt令牌 @Test public void testVerify(){ //公鑰 String publickey = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnASXh9oSvLRLxk901HANYM6KcYMzX8vFPnH/To2R+SrUVw1O9rEX6m1+rIaMzrEKPm12qPjVq3HMXDbRdUaJEXsB7NgGrAhepYAdJnYMizdltLdGsbfyjITUCOvzZ/QgM1M4INPMD+Ce859xse06jnOkCUzinZmasxrmgNV3Db1GtpyHIiGVUY0lSO1Frr9m5dpemylaT0BV3UwTQWVW9ljm6yR3dBncOdDENumT5tGbaDVyClV0FEB1XdSKd7VjiDCDbUAUbDTG1fm3K9sx7kO1uMGElbXLgMfboJ963HEJcU01km7BmFntqI5liyKheX+HBUCD4zbYNPw236U+7QIDAQAB-----END PUBLIC KEY-----"; //jwt令牌 String jwtString = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaXRjYXN0In0.lQOqL1s4DpDHROUAibkz6EMf6hcM7HmTPgmg-SlkacVoQAV7y3XQ7LXxiua6SJlN_uNX_EFjzIshEg_kyy972DtymtRMc2NIO5HzIF5I4oQCxNPsJdhu6qQni6sTas3q0JbAarMZSajDX7HhzVSYWPQJCussA4e1r9oFxDcoAo6TEAXOW8gRHzNIygQz1yCj6mdf4UOHI070kRy7f3BdhmrUJdOuDIMoRBYS4WsEOibAU1UCNPaJAXpZC0ihrtdY7SCg1N43fimeFOHrfpLb6OmRF7v7uvGMgrhg9JIYDbJ6nbode5OJkNceRx8QUICre2yKAe0ctlvXO0REf6OpRA"; //校驗jwt令牌 Jwt jwt = JwtHelper.decodeAndVerify(jwtString, new RsaVerifier(publickey)); //拿到jwt令牌中自定義的內容 String claims = jwt.getClaims(); System.out.println(claims); } @Test public void loadData(){ String path = TestJwt.class.getClassLoader().getResource("publickey.txt").getPath(); System.out.println(path); } }
3、認證服務
1、目錄結構
JwtUser jwt令牌要存儲的對象,以及作為一個UserDetails 的實現類
package test.springsecurity.auth.DTO; /** * jwt令牌中存儲的對象,可以附加自己想要的信息 * * 將這個對象存到jwt中主要是JwtAccessTokenConverter這個對象的DefaultUserAuthenticationConverter來實現的 * */ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import java.util.Collection; public class JwtUser extends User { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) { super(username, password, authorities); } }
1、UserDetailsService
package test.springsecurity.auth.service; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; 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.stereotype.Service; import test.springsecurity.auth.DTO.JwtUser; import java.util.List; @Service public class UserDetailServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { String password = new BCryptPasswordEncoder().encode("123"); List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,queryAllOrder"); JwtUser jwtUser = new JwtUser(s, password, authorities); jwtUser.setName("張三"); return jwtUser; } }
yaml文件配置文件
spring:
application: name: test-auth server: port: 20004 eureka: client: service-url: defaultZone: http://127.0.0.1:20001/eureka instance: lease-renewal-interval-in-seconds: 5 # 5秒鍾發送一次心跳 lease-expiration-duration-in-seconds: 10 # 10秒不發送就過期
#秘鑰相關的配置 ,你可以查看KeyProperties
encrypt: key-store: location: classpath:/xc.keystore secret: xuechengkeystore alias: xckey password: xuecheng
@ConfigurationProperties("encrypt")
public class KeyProperties 使用了這個配置
package test.springsecurity.auth.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.oauth2.provider.token.DefaultAccessTokenConverter; 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 java.security.KeyPair; /** * 提供了
JwtAccessTokenConverter使用證書文件中的私鑰以及我們自定義的規則,將普通token轉為jwttoken
tokenStore tokenStore token的存儲方式
* */ @Configuration public class JwtConfig { //讀取密鑰的配置 @Bean("keyProp") public KeyProperties keyProperties(){ return new KeyProperties(); } @Resource(name = "keyProp") private KeyProperties keyProperties; @Bean @Autowired public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { return new JwtTokenStore(jwtAccessTokenConverter); } @Bean @Autowired public JwtAccessTokenConverter jwtAccessTokenConverter(CustomUserAuthenticationConverter customUserAuthenticationConverter) { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); KeyPair keyPair = new KeyStoreKeyFactory (keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray()) .getKeyPair(keyProperties.getKeyStore().getAlias()); converter.setKeyPair(keyPair); //這個類DefaultAccessTokenConverter負責jwt token的生成,我們可以自定義來添加我們想要的東西 DefaultAccessTokenConverter accessTokenConverter = (DefaultAccessTokenConverter) converter.getAccessTokenConverter(); accessTokenConverter.setUserTokenConverter(customUserAuthenticationConverter); return converter; } }
CustomUserAuthenticationConverter負責jwt token的生成
package test.springsecurity.auth.config; 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.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; import org.springframework.stereotype.Component; import test.springsecurity.auth.DTO.JwtUser; import test.springsecurity.auth.service.UserDetailServiceImpl; import java.util.LinkedHashMap; import java.util.Map; @Component public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter { @Autowired UserDetailServiceImpl userDetailServiceImpl; @Override public Map<String, ?> convertUserAuthentication(Authentication authentication) { LinkedHashMap response = new LinkedHashMap(); String name = authentication.getName(); response.put("user_name", name); Object principal = authentication.getPrincipal(); JwtUser jwtUser = null; if(principal instanceof JwtUser){ jwtUser = (JwtUser) principal; }else{ //refresh_token默認不去調用userdetailService獲取用戶信息,這里我們手動去調用,得到 JwtUser UserDetails userDetails = userDetailServiceImpl.loadUserByUsername(name); jwtUser = (JwtUser) userDetails; } response.put("name", jwtUser.getName()); if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities())); } return response; } }
WebSecurityConfig
package test.springsecurity.auth.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 { @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/userlogin","/userlogout","/userjwt"); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } //采用bcrypt對密碼進行編碼 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .httpBasic().and() .formLogin() .and() .authorizeRequests().anyRequest().authenticated(); } }
最核心的配置 AuthorizationServerConfigpackage test.springsecurity.auth.configimport 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.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 test.springsecurity.auth.service.UserDetailServiceImpl; import javax.annotation.Resource; import javax.sql.DataSource; import java.security.KeyPair; @Configuration @EnableAuthorizationServer class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //jwt令牌轉換器 @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired UserDetailServiceImpl userDetailServiceImpl; @Autowired AuthenticationManager authenticationManager; @Autowired TokenStore tokenStore; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients .inMemory() .withClient("client") .secret(bCryptPasswordEncoder.encode("123")) // .redirectUris("http://www.baidu.com") .redirectUris("http://localhost:20003/login") .accessTokenValiditySeconds(3600) .scopes("all") .authorizedGrantTypes("authorization_code","password","refresh_token"); } //授權服務器端點配置 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.accessTokenConverter(jwtAccessTokenConverter) .authenticationManager(authenticationManager)//認證管理器 .tokenStore(tokenStore)//令牌存儲 .userDetailsService(userDetailServiceImpl);//用戶信息service } //授權服務器的安全配置 @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { // oauthServer.checkTokenAccess("isAuthenticated()");//校驗token需要認證通過,可采用http basic認證 oauthServer.allowFormAuthenticationForClients() .passwordEncoder(new BCryptPasswordEncoder()) //是否可以訪問oauth/token_key :提供公有密匙的端點,使用 JWT 令牌時會使用 , 涉及的類 TokenKeyEndpoint // .tokenKeyAccess("permitAll()") // /oauth/check_token :用於資源服務器請求端點來檢查令牌是否有效, 涉及的類 CheckTokenEndpoint .checkTokenAccess("isAuthenticated()"); }