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()"); }