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