OAuth2.0协议专区-Springcloud集成springsecurity oauth2实现服务统一认证,应该时最简单的教程了~
1.配置认证服务器
(1) 首先配置springsecurity,其实他底层是很多filter组成,顺序是请求先到他这里进行校验,然后在到oauth
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | <strong> /** * @author: gaoyang * @Description: 身份认证拦截 */ @Order ( 1 ) @Configuration //注解权限拦截 @EnableGlobalMethodSecurity (prePostEnabled = true , securedEnabled = true , jsr250Enabled = true ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceConfig userDetailsServiceConfig; //认证服务器需配合Security使用 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super .authenticationManagerBean(); } </strong><br><strong> //websecurity用户密码和认证服务器客户端密码都需要加密算法 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //验证用户权限 auth.userDetailsService(userDetailsServiceConfig); //也可以在内存中创建用户并为密码加密 // auth.inMemoryAuthentication() // .withUser("user").password(passwordEncoder().encode("123")).roles("USER") // .and() // .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN"); } //uri权限拦截,生产可以设置为启动动态读取数据库,具体百度 @Override protected void configure(HttpSecurity http) throws Exception { http //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态 .formLogin() .and() .csrf().disable() .authorizeRequests().antMatchers( "/test" ).permitAll() .and() .authorizeRequests().anyRequest().authenticated(); } //设置不拦截资源服务器的认证请求 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers( "/oauth/check_token" ); } }</strong> |
(2)这里的UserDetailsServiceConfig就是去校验登陆用户,可以写测试使用内存或者数据库方式读取用户信息(我这里写死了账号为user,密码为123)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @Component public class UserDetailsServiceConfig implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; //生产环境使用数据库进行验证 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (!username.equals( "user" )) { throw new AcceptPendingException(); } return new User(username, passwordEncoder.encode( "123" ), AuthorityUtils.commaSeparatedStringToAuthorityList( "ROLE_USER" )); } } |
(3)配置认证服务器(详见注释)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | /** * @author: gaoyang * @Description:认证服务器配置 */ @Order ( 2 ) @EnableAuthorizationServer @Configuration public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired UserDetailsServiceConfig myUserDetailsService; //为了测试客户端与凭证存储在内存(生产应该用数据库来存储,oauth有标准数据库模板) <strong> @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient( "client1-code" ) // client_id .secret(bCryptPasswordEncoder.encode( "123" )) // client_secret .authorizedGrantTypes( "authorization_code" ) // 该client允许的授权类型 .scopes( "app" ) // 允许的授权范围 .redirectUris( "https://www.baidu.com" ) .resourceIds( "goods" , "mechant" ) //资源服务器id,需要与资源服务器对应 .and() .withClient( "client2-credentials" ) .secret(bCryptPasswordEncoder.encode( "123" )) .authorizedGrantTypes( "client_credentials" ) .scopes( "app" ) .resourceIds( "goods" , "mechant" ) .and() .withClient( "client3-password" ) .secret(bCryptPasswordEncoder.encode( "123" )) .authorizedGrantTypes( "password" ) .scopes( "app" ) .resourceIds( "mechant" ) .and() .withClient( "client4-implicit" ) .authorizedGrantTypes( "implicit" ) .scopes( "app" ) .resourceIds( "mechant" ); }</strong> <strong> //配置token仓库 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //authenticationManager配合password模式使用 endpoints.authenticationManager(authenticationManager) //这里使用内存存储token,也可以使用redis和数据库 .tokenStore( new InMemoryTokenStore()); endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); endpoints.tokenEnhancer( new TokenEnhancer() { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { //在返回token的时候可以加上一些自定义数据 DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken; Map<String, Object> map = new LinkedHashMap<>(); map.put( "nickname" , "测试姓名" ); token.setAdditionalInformation(map); return token; } }); }</strong> <strong> //配置token状态查询 @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求 security.allowFormAuthenticationForClients(); } </strong> //以下数据库配置 /** * * @Bean * @Primary * @ConfigurationProperties(prefix = "spring.datasource") * public DataSource dataSource() { * // 配置数据源(注意,我使用的是 HikariCP 连接池),以上注解是指定数据源,否则会有冲突 * return DataSourceBuilder.create().build(); * } * * @Bean * public TokenStore tokenStore() { * // 基于 JDBC 实现,令牌保存到数据 * return new JdbcTokenStore(dataSource()); * } * * @Bean * public ClientDetailsService jdbcClientDetails() { * // 基于 JDBC 实现,需要事先在数据库配置客户端信息 * return new JdbcClientDetailsService(dataSource()); * } * * @Override * public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { * // 设置令牌 * endpoints.tokenStore(tokenStore()); * } * * @Override * public void configure(ClientDetailsServiceConfigurer clients) throws Exception { * // 读取客户端配置 * clients.withClientDetails(jdbcClientDetails()); * } * */ } |
(4) 新增自定义返回认证服务器数据:(这里只做演示,没有合理封装)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | @RestController @RequestMapping ( "/oauth" ) public class CustomResult { @Autowired private TokenEndpoint tokenEndpoint; @GetMapping ( "/token" ) public Object getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return this .result(principal,parameters); } @PostMapping ( "/token" ) public Object postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return this .result(principal,parameters); } public Object result(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.getAccessToken(principal, parameters); OAuth2AccessToken body = accessToken.getBody(); Map<String, Object> customMap = body.getAdditionalInformation(); String value = body.getValue(); OAuth2RefreshToken refreshToken = body.getRefreshToken(); Set<String> scope = body.getScope(); int expiresIn = body.getExpiresIn(); customMap.put( "token" ,value); customMap.put( "scope" ,scope); customMap.put( "expiresIn" ,expiresIn); customMap.put( "refreshToken" ,refreshToken); Map map = new HashMap(); map.put( "code" , 0 ); map.put( "msg" , "success" ); map.put( "data" ,customMap); return map; } } |
(5)添加获取token错误返回:(注意,客户端信息错误这里是拦截不到的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | @RestControllerAdvice public class RestControllerExceptionAdvice { //判断oauth异常,自定义返回数据 @ExceptionHandler public Object exception(OAuth2Exception e){ //if ("invalid_client".equals(errorCode)) { // return new InvalidClientException(errorMessage); // } else if ("unauthorized_client".equals(errorCode)) { // return new UnauthorizedClientException(errorMessage); // } else if ("invalid_grant".equals(errorCode)) { // return new InvalidGrantException(errorMessage); // } else if ("invalid_scope".equals(errorCode)) { // return new InvalidScopeException(errorMessage); // } else if ("invalid_token".equals(errorCode)) { // return new InvalidTokenException(errorMessage); // } else if ("invalid_request".equals(errorCode)) { // return new InvalidRequestException(errorMessage); // } else if ("redirect_uri_mismatch".equals(errorCode)) { // return new RedirectMismatchException(errorMessage); // } else if ("unsupported_grant_type".equals(errorCode)) { // return new UnsupportedGrantTypeException(errorMessage); // } else if ("unsupported_response_type".equals(errorCode)) { // return new UnsupportedResponseTypeException(errorMessage); // } else { // return (OAuth2Exception)("access_denied".equals(errorCode) ? new UserDeniedAuthorizationException(errorMessage) : new OAuth2Exception(errorMessage)); // } return "获取token错误" ; } } |
(6)添加自定义登陆及授权页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | @Controller // 必须配置该作用域设置 @SessionAttributes ( "authorizationRequest" ) public class Oauth2Controller { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @RequestMapping ( "/authentication/require" ) @ResponseBody @ResponseStatus (code = HttpStatus.UNAUTHORIZED) public Map requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if ( null != savedRequest) { String targetUrl = savedRequest.getRedirectUrl(); System.out.println( "引发跳转的请求是:" + targetUrl); redirectStrategy.sendRedirect(request, response, "/ologin" ); } //如果访问的是接口资源 return new HashMap() {{ put( "code" , 401 ); put( "msg" , "访问的服务需要身份认证,请引导用户到登录页" ); }}; } @RequestMapping ( "/ologin" ) public String oauthLogin(){ return "oauthLogin" ; } //授权控制器 @RequestMapping ( "/oauth/confirm_access" ) public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception { Map<String, String> scopes = (Map<String, String>) (model.containsKey( "scopes" ) ? model.get( "scopes" ) : request.getAttribute( "scopes" )); List<String> scopeList = new ArrayList<>(); if (scopes != null ) { scopeList.addAll(scopes.keySet()); } model.put( "scopeList" , scopeList); return "oauthGrant" ; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //uri权限拦截,生产可以设置为启动动态读取数据库,具体百度 @Override protected void configure(HttpSecurity http) throws Exception { http //此处不要禁止formLogin,code模式测试需要开启表单登陆,并且/oauth/token不要放开或放入下面ignoring,因为获取token首先需要登陆状态 .formLogin().loginPage( "/authentication/require" ) .loginProcessingUrl( "/authentication/form" ) .passwordParameter( "password" ) .usernameParameter( "username" ) .and() .csrf().disable() .authorizeRequests().antMatchers( "/test" , "/authentication/require" , "/ologin" ).permitAll() .and() .authorizeRequests().anyRequest().authenticated(); } |
4.配置资源服务器
(1)配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | //配置资源服务器 @Configuration @EnableResourceServer @EnableGlobalMethodSecurity (prePostEnabled = true , securedEnabled = true , jsr250Enabled = true ) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private ObjectMapper objectMapper = new ObjectMapper(); @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { //设置资源服务器id,需要与认证服务器对应 resources.resourceId( "mechant" ); //当权限不足时返回 resources.accessDeniedHandler((request, response, e) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter() .write(objectMapper.writeValueAsString(Result.from( "0001" , "权限不足" , null ))); }); //当token不正确时返回 resources.authenticationEntryPoint((request, response, e) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter() .write(objectMapper.writeValueAsString(Result.from( "0002" , "access_token错误" , null ))); }); } //配置uri拦截策略 @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .httpBasic().disable() .exceptionHandling() .authenticationEntryPoint((req, resp, exception) -> { resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); resp.getWriter() .write(objectMapper.writeValueAsString(Result.from( "0002" , "没有携带token" , null ))); }) .and() //无需登陆 .authorizeRequests().antMatchers( "/noauth" ).permitAll() .and() //拦截所有请求,并且检查sope .authorizeRequests().anyRequest().access( "isAuthenticated() && #oauth2.hasScope('app')" ); } //静态内部返回类 @Data static class Result<T> { private String code; private String msg; private T data; public Result(String code, String msg, T data) { this .code = code; this .msg = msg; this .data = data; } public static <T> Result from(String code, String msg, T data) { return new Result(code, msg, data); } } } |
(2)测试接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @RestController public class TestController { @GetMapping ( "ping" ) public Object test() { return "pong" ; } //无需登陆 @GetMapping ( "noauth" ) public Object noauth() { return "noauth" ; } } |
(3)application.yml配置(远程向认证服务器鉴权)
1 2 3 4 5 6 7 8 9 10 | #配置向认证服务器认证权限 security: oauth2: client: client-id: client3-password client-secret: 123 access-token-uri: http: //localhost:8082/oauth/token user-authorization-uri: http: //localhost:8082/oauth/authorize resource: token-info-uri: http: //localhost:8082/oauth/check_token |
5.测试用例~
(1)password模式
表单方式:(localhost:8082/oauth/token?username=user&password=123&grant_type=password&client_secret=123&client_id=client3-password)
注意需要开启认证服务器的:
1 2 3 4 5 | @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //开启支持通过表单方式提交client_id和client_secret,否则请求时以basic auth方式,头信息传递Authorization发送请求 security.allowFormAuthenticationForClients(); } |
表单加token方式:
(2)code模式
浏览器访问: localhost:8082/oauth/authorize?client_id=client1-code&response_type=code
跳转到登陆页面:
选择允许
然后跳转到之前设置的地址,并携带code:
拿着code请求token:
自定义登陆及授权页面:
## 当前这样配置的话,如果各微服务之间互相调用,则是没有权限的;所以我们可以给他加上token,例如使用feign:
1 2 3 4 5 6 7 | @Component public class OauthConfig implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { requestTemplate.query( "access_token" , "71f422b5-2204-4653-8a85-9cf2c62aac81" ); } } |
这里我写固定了,其实实现的话可以在认证服务器指定一个客户端模式,然后去动态获取token,这个token的过期缓存等等,可以自由发挥;
其实现在很多微服务都是内网通信,通过路由暴露端口了,所以可以定制一些特殊请求,来做无权限访问?
本文来自博客园,作者:洛神灬殇,转载请注明原文链接:https://www.cnblogs.com/liboware/p/12528420.html,任何足够先进的科技,都与魔法无异。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步