赞
踩
整体介绍
security-oauth2 提供了一套完善的token认证授权体系,同时也提供一些配置类,让我们继承,重写的手段,做自己想要的配置。token的存储方式有多种,本章内容只讲解JWT存储方式的配置
打开黑窗口,运行命令:keytool -genkeypair -alias 【别名】-keyalg 【加密算法】 -keypass 【私钥的密码】 -keystore 【密钥库的名称】.【密钥库的后缀】 -storepass 【密钥库口令】
完整案列:keytool -genkeypair -alias alias -keyalg RSA -keypass keypass -keystore test.keystore -storepass storepass
命令执行之后,会弹出如下窗口
最后输入y,敲回车结束窗口,随即会在黑窗口当前目录下产生一个名为"test.keystore"的文件,这个就是密钥,其中包含了私钥和公钥
继续执行命令(查看密钥):keytool -list -rfc --keystore test.keystore | openssl x509 -inform pem -pubkey
会看到如下内容:
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi4gx+aC53QDguQ0MWaBe cRM62P8mEAQSQsp9wrn2Sfa+DQX3pUB2HxpnDYehSvkNCjn3cFrCIFf4r4szopAd Ay6SNblETXD8td1nzNTRdC/uR4uzUo3JCvz1uYpu9hEfPmmvkC6YehnjIRP13HOh WzdnW6qdGB15CDlvySjDxli1LX6xbKYXZ6NjC8J5Zb8xkjXOEiiOqpNwwtDlYL/R 1eSCvCJWiOQBGKl9/cjFW38B4us6l2m4L8dW6fzbFwC/lsAIFydvP0X/qdjN3htg YivJP1MQVc9Nb8Q/Xs5drQcqZeZ5VLK7lzy8MkcjG2dA8XWswRmNIUxyAH7jSpzf OQIDAQAB -----END PUBLIC KEY----- -----BEGIN CERTIFICATE----- MIIDQzCCAiugAwIBAgIEcTS6tjANBgkqhkiG9w0BAQsFADBSMQswCQYDVQQGEwJj bjELMAkGA1UECBMCc2gxCzAJBgNVBAcTAnNoMQ0wCwYDVQQKEwRoYW5kMQ0wCwYD VQQLEwRoYW5kMQswCQYDVQQDEwJldjAeFw0xOTA1MzAwNzAzMTdaFw0xOTA4Mjgw NzAzMTdaMFIxCzAJBgNVBAYTAmNuMQswCQYDVQQIEwJzaDELMAkGA1UEBxMCc2gx DTALBgNVBAoTBGhhbmQxDTALBgNVBAsTBGhhbmQxCzAJBgNVBAMTAmV2MIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi4gx+aC53QDguQ0MWaBecRM62P8m EAQSQsp9wrn2Sfa+DQX3pUB2HxpnDYehSvkNCjn3cFrCIFf4r4szopAdAy6SNblE TXD8td1nzNTRdC/uR4uzUo3JCvz1uYpu9hEfPmmvkC6YehnjIRP13HOhWzdnW6qd GB15CDlvySjDxli1LX6xbKYXZ6NjC8J5Zb8xkjXOEiiOqpNwwtDlYL/R1eSCvCJW iOQBGKl9/cjFW38B4us6l2m4L8dW6fzbFwC/lsAIFydvP0X/qdjN3htgYivJP1MQ Vc9Nb8Q/Xs5drQcqZeZ5VLK7lzy8MkcjG2dA8XWswRmNIUxyAH7jSpzfOQIDAQAB oyEwHzAdBgNVHQ4EFgQUarJgnmi/5Xk6lGlr0BXjZ1VPxpUwDQYJKoZIhvcNAQEL BQADggEBABbn5ORbO+xAG1DNVOp8BKsdkoDkB9WdTRwtIvqkSWq55lF0Rhbs8NPM A/CTUPIfHTEl8ACxTKBORLhtmY2O9wwxTX0iSVUgfPzBAoOldb9FfXhf6OJpgrbT Qz8rNbXMgvdRppZ7RH6Uqmv74HBoueMKfAKOFqh3kVjnsumWZOvoFIJDcx9z9Uc8 hLoNUBTdvNL3RlpGJBc55vPHVs47RUqOX5iH33w+mnEM4Jg9bB3EGjCBi2yqeHog y8JpqcJLrgtIugDH+pbDRd4RbYRzfEah0S7oHMTNbUmZBUK+c+reZgFCbHal+yup gRannJ06ijk8Jcc2Xs20HFQ9FYuoclE= -----END CERTIFICATE-----
上面内容中的 BEGIN PUBLIC KEY 至 END PUBLIC KEY的部分,就是密钥中的公钥 (公钥可以先粘贴复制出来,后需要用到)
将上一步骤中的"test.keystore"文件放在 maven的resources资源目录下;
token(令牌)存储方式有JWT存储,JDBC存储,内存存储,redis存储及其他存储方式
JWT( json web token)是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范,也是一种token。
JWT由三部分组成:
Header 头部:算法和存储类型信息
Payload 负载:身份信息或者自定义的信息
Signature 签名: RSA签名信息(前文中的密钥,也是通过RSA算法生成的)
使用JWT生成的token大致长这样:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ctMSI6Iumdk-S7lCIsIm1zZy0yIjoi6Z2T5aWzIiwidXNlcl9uYW1lIjoiZXYiLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU1OTM2NjA1OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjVhMDdjYmUyLTEzNDYtNDkwNi1hNDE3LWIxMGYyNWEzM2FjMSIsImNsaWVudF9pZCI6ImNsaWVudEVWIn0.g774D_ITXavxxHdeZ11X_RywCVAMTRrkMpEcCkMMZyk8BYrlHGmTb3ACIeAOWEIyNLXJe06c4vYZaEfrFobXwvFaaH8nZtOUyK_72HziVPfigstOI-ehFivUtL–BPvsERF39gH26yAY_tjWNFhqu8MYo2CvlZMEWGBTdnd5tK8hcq8_w85KjXoggVt-7KXYYnlLVeo-xJOvsEeNpaor0VzFIN61Dfbnj-nB0CaPxsxlT8WC_33AbXvtHnJVqPzQIA3lKRhRaGuRGExZx_BDhL531T2NMlP-NkzRxrausf5fSTegfa-bjrNZh9dg-Pxv3yzRyFIfDxkJ-BJMaQR7KQ
上面这个token,是一个json字符串通过RSA算加密得来的,而这个json字符串是可以通过 https://jwt.io 解析出来的
解析后得到的json字符串大概长这样(下文会讲解这个json字符串是怎么来的):
{"msg-1":"靓仔","msg-2":"靓女","user_name":"ev","scope":["read"],"exp":1559377994,"authorities":["ROLE_USER"],"jti":"dccd734e-5c43-4018-bc62-addefb429c56","client_id":"clientEV"}
上面这个token以“.”分割成三部分:分别对应JWT的三个结构:
Header :eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9 --> 解码 --> 得到算法和存储类型信息
Payload : eyJtc2ctMSI6Iumdk-S7lCIsIm1zZy0yIjoi6Z2T5aWzIiwidXNlcl9uYW1lIjoiZXYiLCJzY29wZSI6WyJyZWFkIl0sImV4cCI6MTU1OTM2NjA1OCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIl0sImp0aSI6IjVhMDdjYmUyLTEzNDYtNDkwNi1hNDE3LWIxMGYyNWEzM2FjMSIsImNsaWVudF9pZCI6ImNsaWVudEVWIn0 --> 解码 --> 得到上面这样的json字符串
Signature :g774D_ITXavxxHdeZ11X_RywCVAMTRrkMpEcCkMMZyk8BYrlHGmTb3ACIeAOWEIyNLXJe06c4vYZaEfrFobXwvFaaH8nZtOUyK_72HziVPfigstOI-ehFivUtL--BPvsERF39gH26yAY_tjWNFhqu8MYo2CvlZMEWGBTdnd5tK8hcq8_w85KjXoggVt-7KXYYnlLVeo-xJOvsEeNpaor0VzFIN61Dfbnj-nB0CaPxsxlT8WC_33AbXvtHnJVqPzQIA3lKRhRaGuRGExZx_BDhL531T2NMlP-NkzRxrausf5fSTegfa-bjrNZh9dg-Pxv3yzRyFIfDxkJ-BJMaQR7KQ --> 解码 --> 签名算法
核心配置之一,代码块中有注释,这里只需要知道这里怎么配置的,配置起什么作用就行了,具体配置是何时被用到的,不是本章重点,会在本人博客的其它文章中详细说明
@Component public class TokenStoreConfig { @Bean("keyProp") public KeyProperties keyProperties() { return new KeyProperties(); } //前文中生成的 test.keystore 文件在resources资源文件夹下的路径 private String location = "test.keystore"; // 生成 test.keystore 文件时候所用的【密钥库口令】 private String secret = "storepass"; // 生成 test.keystore 文件时候所用的【别名】 private String alias = "alias"; // 生成 test.keystore 文件时候所用的【私钥的密码】 private String password = "keypass"; // 生成的秘钥中的 PUBLIC KEY 部分 private String publicKey = "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsn4FPljPEtJCDmXMEoEK\n" + "xLCMl5rmd8MUxFVBhwUZGSH71h1Zphaxm9EvSulJQm+FzcldJYTeIDcQYtEl1kbQ\n" + "4Fmq840Iy3JSLF1Z9WXpddIzB4PLkrJwFYAXZAR351jbYGqUbzWWjJEOGLP6Yvzb\n" + "BrN6DzEYamoSwGUl8R93h1oRUHR3tVqdbMcjmyllvY7N9Qgk9lAldm6CcgavMqbm\n" + "USp2X64CgAtdwDixWSO3eCucpXX6ocwaEnUB4XWTXl76OhilZ0WSXXHXOuDZGllj\n" + "7fhmL0Hyo4bfy3PuQ9BdCJqE54SIELGxrTvapgLqwWhngJlnHSAGqRMkNxCFmwCk\n" + "/wIDAQAB\n" + "-----END PUBLIC KEY-----"; /** * TokenStore是一个接口,他有五个直接子类,分别是: * JwtTokenStore jwt方式存储 * JdbcTokenStore 数据库方式存储 * InMemoryTokenStore 内存方式存储 * 这里指定了是以JWT方式存储 */ @Bean public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) { //JWT方式存储,需要JWT令牌转换器,下面jwtAccessTokenConverter()方法中自定义了一个JWT令牌转换器 return new JwtTokenStore(jwtAccessTokenConverter); } /** * 自定义JWT令牌转换器,交给Spring * JWT令牌转换器需要密钥对 * 密钥对需要密钥文件,即( .jks 或者 .keystore) 文件,该文件里保存了密钥信息,密钥包含了私钥和公钥 * * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new MyJwtAccessTokenConverter(); // ================= 加密相关配置 ===================== ClassPathResource resource = new ClassPathResource(location); KeyStoreKeyFactory keyStoreFactory = new KeyStoreKeyFactory(resource, secret.toCharArray()); KeyPair keyPair = keyStoreFactory.getKeyPair(alias, password.toCharArray()); //为自定义的JWT令牌转换器设置密钥对(密钥对包含了公钥和私钥,公钥用于加密令牌,私钥用于解密令牌) jwtAccessTokenConverter.setKeyPair(keyPair); // ================= 解密相关配置 ====================== DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter(); defaultAccessTokenConverter.setUserTokenConverter(new MyDefaultUserAuthenticationConverter()); jwtAccessTokenConverter.setAccessTokenConverter(defaultAccessTokenConverter); jwtAccessTokenConverter.setVerifierKey(publicKey); return jwtAccessTokenConverter; } class MyDefaultUserAuthenticationConverter extends DefaultUserAuthenticationConverter { //在token的解密过程中,可以在extractAuthentication()方法中处理从token中解析出来的用户信息 @Override public Authentication extractAuthentication(Map<String, ?> map) { if (map.containsKey(USERNAME)) { List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils .collectionToCommaDelimitedString((Collection<?>) map.get(AccessTokenConverter.AUTHORITIES))); // CustomUserDetails 是自定义的实体类,继承了 User 类,实现自定义用户信息 CustomUserDetails userDetails = new CustomUserDetails((String) map.get(USERNAME), "admin", authorities); userDetails.setOrganizationName("智商有限公司"); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, "N/A", authorities); return usernamePasswordAuthenticationToken; } return null; } } class MyJwtAccessTokenConverter extends JwtAccessTokenConverter { //在token的加密过程中,可以在enhance()方法中处理即将加密到token中的用户信息 @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken); Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation()); String tokenId = result.getValue(); if (!info.containsKey(TOKEN_ID)) { info.put(TOKEN_ID, tokenId); } // ==================================== info.put("age", "18");//添加年龄,18岁 // ==================================== result.setAdditionalInformation(info); //super.encode(result, authentication) 用于加密信息 result.setValue(super.encode(result, authentication)); result.setRefreshToken(accessToken.getRefreshToken()); result.setExpiration(accessToken.getExpiration()); return result; } } }
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private ApplicationContext applicationContext; //上文中配置的 TokenStore @Autowired private TokenStore tokenStore; @Autowired private OAuth2ClientProperties oAuth2ClientProperties; @Autowired private AuthenticationManager authenticationManager; @Autowired(required = false) private JdbcClientDetailsService jdbcClientDetailsService; //令牌失效时间 public int accessTokenValiditySeconds = 3600; //刷新令牌失效时间 public int refreshTokenValiditySeconds; //是否可以重用刷新令牌 public boolean isReuseRefreshToken; //是否支持刷新令牌 public boolean isSupportRefreshToken; public AuthorizationServerConfig() { this((int) TimeUnit.DAYS.toSeconds(1), 0, false, false); } public AuthorizationServerConfig(int accessTokenValiditySeconds, int refreshTokenValiditySeconds, boolean isReuseRefreshToken, boolean isSupportRefreshToken) { this.accessTokenValiditySeconds = accessTokenValiditySeconds; this.refreshTokenValiditySeconds = refreshTokenValiditySeconds; this.isReuseRefreshToken = isReuseRefreshToken; this.isSupportRefreshToken = isSupportRefreshToken; } /** * 配置授权服务器端点,如令牌存储,令牌自定义,用户批准和授权类型,不包括端点安全配置 */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); //设置token存储方式,在AuthJWTToken中定义了JwtTokenStore方式存储 defaultTokenServices.setTokenStore(tokenStore); /** * 设置token"加强者",因为是jwt方式存储,所以需要一个jwt令牌转换器,JwtAccessTokenConverter继承了TokenEnhancer * 而TokenEnhancer中有enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)方法,该方法给加强token * 下面这句代码,会找到TokenEnhancer接口的实现类(AuthJWTTokenStoreDecode.MyJwtAccessTokenConverter extends JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean) */ Collection<TokenEnhancer> tokenEnhancers = applicationContext.getBeansOfType(TokenEnhancer.class).values(); TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(new ArrayList<>(tokenEnhancers)); defaultTokenServices.setTokenEnhancer(tokenEnhancerChain); /** * 设置默认值 */ defaultTokenServices.setReuseRefreshToken(isReuseRefreshToken); defaultTokenServices.setSupportRefreshToken(isSupportRefreshToken); defaultTokenServices.setAccessTokenValiditySeconds(accessTokenValiditySeconds); defaultTokenServices.setRefreshTokenValiditySeconds(refreshTokenValiditySeconds); /*//若通过 JDBC 存储令牌 if (Objects.nonNull(jdbcClientDetailsService)){ defaultTokenServices.setClientDetailsService(jdbcClientDetailsService); }*/ /** * 如果用密码模式验证,则需要配置AuthenticationManager */ endpoints .authenticationManager(authenticationManager) //todo : 不知道authenticationManager的实现类是哪一个 .userDetailsService(new CustomUserDetailsServiceImpl()) .tokenServices(defaultTokenServices); } /** * 配置客户端详情 * * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // 使用内存存储客户端信息 .withClient(oAuth2ClientProperties.getClientId()) // client_id /* * security5.0版本之后,如果DelegatingPasswordEncoder的默认密码加密是BCryptPasswordEncoder , 那么此处必须给secret加密,因为再源码中会校验它 * */ .secret(new BCryptPasswordEncoder().encode(oAuth2ClientProperties.getClientSecret())) // client_secret .authorizedGrantTypes("authorization_code", "password") // 该client允许的授权类型 .accessTokenValiditySeconds(3600) // Token 的有效期 .scopes("read") // 允许的授权范围 .autoApprove(true); //登录后绕过批准询问(/oauth/confirm_access) } /** * 配置授权服务器端点的安全 * * @param oauthServer * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { /** * 默认访问安全规则是denyAll(),即默认情况下是关闭的,请求调不通 * 在资源服务器中,可能会调用oauth/token_key 来请求获取公钥,如果这里调不通,资源服务就没办法获取公钥,就没办法对jwt令牌的签名算法进行解密 */ oauthServer .tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()"); }
AuthorizationServerConfigurerAdapter 的实例是通过@Configuration注解的形式注入到spring中的,它主要有三个配置方法:
代码块中有注释进行具体解释,阅读注释即可
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 设置defaultPasswordEncoderForMatches为NoOpPasswordEncoder * https://blog.csdn.net/alinyua/article/details/80219500 security5.0版本之后有所改动,这个网址讲解的很到位 * * @return */ @Bean public PasswordEncoder passwordEncoder() { FastJsonHttpMessageConverter a; MappingJackson2HttpMessageConverter b; WebMvcConfigurer c; DelegatingPasswordEncoder delegatingPasswordEncoder = (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder(); delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(new BCryptPasswordEncoder()); /** * NoOpPasswordEncoder被认为是不安全的,因为源码里面,实际没有编码密码 * delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance()); */ return delegatingPasswordEncoder; } @Override protected void configure(HttpSecurity http) throws Exception { //如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击。 // Spring Security3.2开启,默认会启用CSRF防护 http.csrf().disable(); } /** * 跨域请求 * 出于安全考虑,浏览器会限制从脚本内发起的跨域HTTP请求。跨域资源共享机制允许 Web 应用服务器进行跨域访问控制, * 从而使跨域数据传输得以安全进行。浏览器支持在 API 容器中使用 CORS,以降低跨域 HTTP 请求所带来的风险 * @return */ /* @Bean public CorsFilter corsFilter() { final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); final CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }*/ }
** * 资源服务器通过 @EnableResourceServer 注解来开启一个 OAuth2AuthenticationProcessingFilter 类型的过滤器 * 通过继承 ResourceServerConfigurerAdapter 类来配置资源服务器 */ @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { // @Bean @Qualifier("authorizationHeaderRequestMatcher") public RequestMatcher authorizationHeaderRequestMatcher() { //初始化一个名为authorizationHeaderRequestMatcher的匹配器,注入到spring中 return new RequestHeaderRequestMatcher("Authorization"); } @Override public void configure(HttpSecurity http) throws Exception { http //.requestMatcher(new OAuth2RequestedMatcher()) .authorizeRequests() //接口路径包含“/auth/token"或者“/websocket”的请求,不需要校验token .antMatchers("/auth/token", "/websocket").permitAll() //生成 FilterSecurityInterceptor实例(过滤器链中的其中一个过滤器) //除了上面指定不需要校验token的请求,其它所有请求都必须校验token .anyRequest().authenticated(); //.antMatchers("/**").hasRole("USER");// 要求接口请求头信息中的token所携带的用户信息中,必须有“USER“角色 } @Override public void configure(ResourceServerSecurityConfigurer resources) { } /** * 自定义OAuth2请求匹配器 */ /* private static class OAuth2RequestedMatcher implements RequestMatcher { @Override public boolean matches(HttpServletRequest request) { String auth = request.getHeader("Authorization"); //判断来源请求是否包含oauth2授权信息,这里授权信息来源可能是头部的Authorization值以Bearer开头,或者是请求参数中包含access_token参数,满足其中一个则匹配成功 boolean haveOauth2Token = (auth != null) && auth.startsWith("Bearer"); boolean haveAccessToken = request.getParameter("access_token")!=null; return haveOauth2Token || haveAccessToken; } }*/ }
如果文章内容有误,或者有更好的理解,欢迎交流,谢谢!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。