密码模式获取token
postman
- 测试访问:localhost:7070/auth/oauth/token,post请求
- body(form-data)
- grant_type = password
- scope = all (匹配的字符串,存储在认证服务器中,也是在代码中配置的)
- username = James (平台用户)
- password = 123456 (平台用户)
- Authorization,Basic Auth,Base64加密,Base64(id:password),把加密完的信息放到请求头,通过按一下"Preview Request"就可以实现
- Username = pc
- Password = 123456
- body(form-data)
特点
- 粒度比客户端更细,客户端模式下一个客户端就只有一个token,不能区分用户,而密码模式是每个用户都有单独的token
micro-security-db
AuthorizationServerConfiguration
-
package com.xiangxue.jack.config; import com.xiangxue.jack.service.UserServiceDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; 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.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; import javax.sql.DataSource; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private TokenStore tokenStore; @Autowired private UserServiceDetail userServiceDetail; @Autowired private ClientDetailsService clientDetailsService; static final Logger logger = LoggerFactory.getLogger(AuthorizationServerConfiguration.class); // 使用数据库存储 @Bean public TokenStore tokenStore() { return new JdbcTokenStore(dataSource); } @Bean // 声明 ClientDetails实现 public ClientDetailsService clientDetailsService() { return new JdbcClientDetailsService(dataSource); } // 类似spring的钩子方法,springmvc的转换器、自己的过滤器,才能放到spring的容器中去 // 这里就是oauth2.0的钩子方法 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.withClientDetails(clientDetailsService); } // 也是一个钩子方法 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // redisTokenStore // endpoints.tokenStore(new MyRedisTokenStore(redisConnectionFactory)) // .authenticationManager(authenticationManager) // .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); // 存数据库 // tokenStore是用来存储token的 // 1.声明了token的存储方式 // 2.定义了权限校验的管理器 // 3.定义了用户校验的service endpoints.tokenStore(tokenStore) .authenticationManager(authenticationManager) .userDetailsService(userServiceDetail); // 配置tokenServices参数 // tokenServices是用来控制token属性的 DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(endpoints.getTokenStore()); //支持refreshtoken tokenServices.setSupportRefreshToken(true); tokenServices.setClientDetailsService(endpoints.getClientDetailsService()); tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); tokenServices.setAccessTokenValiditySeconds(60 * 5); //重复使用 tokenServices.setReuseRefreshToken(false); tokenServices.setRefreshTokenValiditySeconds(60 * 10); endpoints.tokenServices(tokenServices); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // 允许表单认证 security.allowFormAuthenticationForClients() .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } }
SecurityConfiguration
-
package com.xiangxue.jack.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // 密码加密 // postman传的明明是明文,但是数据库存的是密文 @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { AuthenticationManager manager = super.authenticationManagerBean(); return manager; } @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers().anyRequest() .and() .authorizeRequests() // .antMatchers("/oauth/**").permitAll(); .antMatchers("/").authenticated(); /* http.csrf().disable().exceptionHandling() .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .authorizeRequests(). antMatchers("/favicon.ico").permitAll() .antMatchers("/oauth/**").permitAll() .antMatchers("/login/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .and() .httpBasic().disable();*/ http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic(); //拦截所有请求 通过httpBasic进行认证 } }
UserServiceDetail
-
package com.xiangxue.jack.service; import com.xiangxue.jack.dao.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class UserServiceDetail implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepository.findByUsername(username); } }
- UserDetailsService是oauth2.0中的接口
micro-web-secutiry(鉴权客户端)
OAuth2ClientConfig
-
package com.xiangxue.jack.oauth2; import feign.RequestInterceptor; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; /* 鉴权过滤器 * @ OAuth2AuthenticationProcessingFilter * * */ @EnableOAuth2Client @EnableConfigurationProperties @Configuration public class OAuth2ClientConfig { @Bean @ConfigurationProperties(prefix = "security.oauth2.client") public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { return new ClientCredentialsResourceDetails(); } // @Bean public RequestInterceptor oauth2FeignRequestInterceptor(ClientCredentialsResourceDetails clientCredentialsResourceDetails) { return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails); } @Bean public OAuth2RestTemplate clientCredentialsRestTemplate() { return new OAuth2RestTemplate(clientCredentialsResourceDetails()); } }
- @EnableOAuth2Client
ResourceServerConfiguration
-
package com.xiangxue.jack.oauth2; import org.springframework.context.annotation.Configuration; 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.config.annotation.web.configurers.ResourceServerSecurityConfigurer; @Configuration @EnableResourceServer //启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤 @EnableGlobalMethodSecurity(prePostEnabled = true) public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { // @Autowired // private TokenStore tokenStore; @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable(); // 配置order访问控制,必须认证后才可以访问 // "/order/**","/user/**"路径的需要权限校验,其余的不需要 http.authorizeRequests() .antMatchers("/order/**","/user/**").authenticated(); } /* * 把token验证失败后,重新刷新token的类设置到 OAuth2AuthenticationProcessingFilter * token验证过滤器中 * */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); resources.authenticationEntryPoint(new RefreshTokenAuthenticationEntryPoint()); // resources.tokenStore(tokenStore); } }
-
//启用全局方法安全注解,就可以在方法上使用注解来对请求进行过滤
@EnableGlobalMethodSecurity(prePostEnabled = true)-
@PreAuthorize("hasRole('ROLE_ADMIN')") @RequestMapping("/queryUser") public List<ConsultContent> queryUser(HttpServletRequest request) { request.getCookies(); Enumeration<String> headerNames = request.getHeaderNames(); while(headerNames.hasMoreElements()) { String headername = headerNames.nextElement(); log.info("header name = >" + headername + "= >" + "headervalue = " + request.getHeader(headername)); } return userService.queryContents(request); }
-
@PreAuthorize(“hasRole(‘ROLE_ADMIN’)”),在方法调用之前进行角色校验
-
-
配置文件
-
#security.basic.enabled=true #spring.security.user.name=micro-web #spring.security.user.password=123 #security.oauth2.resource.jwt.key-uri=http://127.0.0.1:3030/oauth/token_key #security.oauth2.resource.service-id=api-gateway #这个是必配的,用来进行token校验,其它的可配可不配 security.oauth2.resource.user-info-uri=http://127.0.0.1:7070/auth/security/check security.oauth2.resource.prefer-token-info=false #security.oauth2.client.id=micro-web security.oauth2.client.clientId=micro-web security.oauth2.client.client-secret=123456 security.oauth2.client.access-token-uri=http://api-gateway/auth/oauth/token security.oauth2.client.grant-type=client_credentials security.oauth2.client.scope=all
- 在下游系统之前有一个filter,zuul去调下游系统时会被filter拦截,会往认证服务器去校验token,就是上面标示的必配内容—security.oauth2.resource.user-info-uri=http://127.0.0.1:7070/auth/security/check,请求又会被认证服务器的filter拦截,进行token校验,如果校验通过就会访问必配的请求url,就会把token从数据库查询到的信息返回给下游系统
- token会查询这张表—oauth_client_token表,同时会把用户相关的角色信息也一起返回回去
- 在下游系统之前有一个filter,zuul去调下游系统时会被filter拦截,会往认证服务器去校验token,就是上面标示的必配内容—security.oauth2.resource.user-info-uri=http://127.0.0.1:7070/auth/security/check,请求又会被认证服务器的filter拦截,进行token校验,如果校验通过就会访问必配的请求url,就会把token从数据库查询到的信息返回给下游系统
测试
-
把github中的api-gateway.properties中的路由配置信息修改成密码模式下的module
-
依次启动Eureka、MicroConfigServer、MicroSecurityDb(认证服务器)、MicroZuulSecurity、MicroWebSecurity、MicroOrderSecurity
-
测试访问:localhost:7070/auth/oauth/token,post请求
- body(form-data)
- grant_type = password
- scope = all (匹配的字符串,存储在认证服务器中,也是在代码中配置的)
- username = James (平台用户)
- password = 123456 (平台用户)
- Authorization,Basic Auth,Base64加密,Base64(id:password),把加密完的信息放到请求头,通过按一下"Preview Request"就可以实现
- Username = pc
- Password = 123456
- body(form-data)
-
localhost:7070/web/user/query/queryUser可以访问成功,但是localhost:7070/web/user/query/bb访问失败,因为配置的权限是ROLE_ADMIN,但是第二个接口的权限是@PreAuthorize(“hasRole(‘ROLE_USER’)”)
授权码模式
- 先获取授权码,然后再通过授权码去获取token
postman
获取code
- 测试访问:localhost:7070/auth/oauth/authorize?client_id=pc&response_type=code&redirect_url=http://localhost:8083/login/callback,get请求
- params
- client_id= pc
- response_type= code
- redirect_url= http://localhost:8083/login/callback
- params
- 在浏览器中访问后,会弹出输入框,需要输入账号和密码,而账号和密码就是user表中的配置,输入账号jack和密码123456,就会进入授权码默认页面,是或授权获取,点击授权按钮后会自动附上授权码并调用写入的回调地址
根据code获取token
-
测试访问:localhost:7070/auth/oauth/token,post请求
- body(form-data)
- grant_type = authorization_code
- redirect_url = localhost:8083/login/callback (与获取code的回调地址保持一致)
- code = te2iN4
- Authorization,Basic Auth,Base64加密,Base64(id:password),把加密完的信息放到请求头,通过按一下"Preview Request"就可以实现
- Username = pc
- Password = 123456
- body(form-data)
-
注意通过code只能拿一次token,再调用就获取不到了,测试时,在micro-web-security项目中,已经配置有localhost:8083/login/callback这个项目,会自动拼接号根据code获取token的http调用,所以这里手动再通过postman调用是不需要的
-
注意上一步通过浏览器访问的,有手动确认的过程,可以在oauth_client_details表中配置自动确认,就不需要浏览器上手动确认了
安全校验总结
客户端模式
- 有一些客户端访问并不需要客户登陆,运行游客登陆,不需要注册的这种,就可以使用客户端模式
密码模式
- 比客户端模式控制粒度更细,获取token是跟用户绑定的,每一个用户都有一个token,根据token可以获取到用户信息,根据用户信息可以获取用户角色
- 存在风险,如果客户端密码被盗
授权码模式
- 回调时需要有授权码,安全性更高,即回调地址是跟用户绑定的,先去获取授权码,再通过授权码去获取token
缺点
- 这三种模式都有一个缺点,多了一个下游系统和认证服务器的权限校验过程(通信交互),如果认证服务器挂了,整个系统就不可用了
- security.oauth2.resource.user-info-uri=http://127.0.0.1:7070/auth/security/check这是校验token的配置,这里的域名和端口不能用服务名代替,只能配ip,可以用域名服务器做转发,并且配置监控keepalive的脚本,就是ps -ef看进程是否在正常运行
总结
- token验证的过滤器OAuth2AuthenticationProcessingFilter
对oauth改造-----token过期怎么处理
-
一般常规的做法是写定时器刷新token
-
package com.xiangxue.jack.oauth2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator; import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint; import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.servlet.ServletException; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Map; public class RefreshTokenAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint { @Autowired private ClientCredentialsResourceDetails clientCredentialsResourceDetails; private WebResponseExceptionTranslator<?> exceptionTranslator = new DefaultWebResponseExceptionTranslator(); @Autowired RestTemplate restTemplate; private static String oauth_server_url = "http://micro-security-db/oauth/token"; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { try { //解析异常,如果是401则处理 ResponseEntity<?> result = exceptionTranslator.translate(authException); if (result.getStatusCode() == HttpStatus.UNAUTHORIZED) { MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>(); formData.add("client_id", clientCredentialsResourceDetails.getClientId()); formData.add("client_secret", clientCredentialsResourceDetails.getClientSecret()); formData.add("grant_type", clientCredentialsResourceDetails.getGrantType()); formData.add("scope", String.join(",",clientCredentialsResourceDetails.getScope())); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); Map map = restTemplate.exchange(oauth_server_url, HttpMethod.POST, new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody(); //如果刷新异常 if (map.get("error") != null) { // 返回指定格式的错误信息 response.setStatus(401); response.setHeader("Content-Type", "application/json;charset=utf-8"); response.getWriter().print("{\"code\":1,\"message\":\"" + map.get("error_description") + "\"}"); response.getWriter().flush(); //如果是网页,跳转到登陆页面 //response.sendRedirect("login"); } else { //如果刷新成功则存储cookie并且跳转到原来需要访问的页面 for (Object key : map.keySet()) { response.addCookie(new Cookie(key.toString(), map.get(key).toString())); } request.getRequestDispatcher(request.getRequestURI()).forward(request, response); // response.sendRedirect(request.getRequestURI()); //将access_token保存 } } else { //如果不是401异常,则以默认的方法继续处理其他异常 super.commence(request, response, authException); } } catch (Exception e) { e.printStackTrace(); } } }
- OAuth2AuthenticationEntryPoint.commence是校验token发生异常catch后会调用的方法,因此新增一个类,并继承OAuth2AuthenticationEntryPoint重新commence方法
- 这样就可以无缝刷新token