赞
踩
OAuth的思路
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料
OAuth 2.0的运行流程如下图,摘自RFC 6749。
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求
工作原理
OAuth 2.0授权框架定义了四种标准授权类型:授权码、隐式、资源所有者密码凭据和客户端凭据
AuthorizationGrantType
1.implicit
2.refresh_token
3.client_credentials
4.password
OAuth 定义了四个角色:
resource owner(资源所有者)
能够授予对受保护资源的访问权限的实体。
当资源所有者是一个人时,它被称为
最终用户。
resource server(资源服务器)
托管受保护资源的服务器,能够使用访问令牌接受
和响应受保护资源请求。
client(客户端)
代表
资源所有者并经其授权发出受保护资源请求的应用程序。“客户”一词确实
不暗示任何特定的实现特征(例如,
应用程序是否在服务器、桌面或其他
设备上执行)。
authorization server(授权服务器)
服务器 在成功验证资源所有者并获得授权
后向客户端颁发访问令牌。
授权服务器和资源服务器之间的交互
超出了本规范的范围。授权服务器
可以是与资源服务器相同的服务器,也可以是单独的实体。
单个授权服务器可以发布多个资源服务器
接受的访问令牌。
流程包括以下步骤:
(A) 客户端通过将资源所有者的用户代理定向到授权端点来
启动流程。客户端包括
其客户端标识符、请求的范围、本地状态和 一旦授予
(或拒绝)访问权限 ,授权服务器会将用户
代理发送回该URI
(B) 授权服务器验证资源所有者(通过用户代理 )
并确定资源所有者是允许还是拒绝客户端的访问请求。
(C) 假设资源所有者授予访问权限,授权服务器使用
之前提供的重定向 URI(在请求中或在
客户端注册期间)
将用户代理重定向回客户端。重定向 URI 包括
授权代码和客户端
之前提供的任何本地状态。
(D) 客户端通过包含
在上一步中收到
的授权码,从授权服务器的令牌端点请求访问令牌。发出请求时,
客户端向授权服务器进行身份验证。客户端
包含用于获取授权的重定向URI验证码。
(E) 授权服务器对客户端进行身份验证,验证 授权码,并确保接收到的重定向 URI与步骤 (C) 中用于重定向客户端的 URI 匹配。如果有效,授权 服务器 将使用访问令牌和可选的刷新令牌进行响应 。 . 授权请求客户端通过 使用“application/x-www-form-urlencoded”格式 将以下参数添加到授权端点 URI 的查询组件来构造请求 URI , response_type REQUIRED. Value MUST be set to "code". client_id REQUIRED. The client identifier as described in Section 2.2. redirect_uri OPTIONAL. As described in Section 3.1.2.
令牌端点TokenEndpoint
授权端点 AuthorizationEndpoint
用户授权提交端点 WhitelabelApprovalEndpoint
JWT内容增强器配置
实现TokenEnhancer接口
package com.macro.mall.auth.component; import com.macro.mall.auth.domain.SecurityUser; import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.OAuth2Authentication; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; /** * JWT内容增强器 * Created by macro on 2020/6/19. */ @Component public class JwtTokenEnhancer implements TokenEnhancer { /** * 在创建供客户端使用的新令牌的过程中,提供定制访问令牌的机会(例如,通过其附加信息映射)。 * @param accessToken 当前访问令牌及其过期和刷新令牌 * @param authentication 当前身份验证 * @return 包括客户端和用户详细信息 */ @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); Map<String, Object> info = new HashMap<>(); //把用户ID设置到JWT中 info.put("id", securityUser.getId()); info.put("client_id",securityUser.getClientId()); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info); return accessToken; } }
SpringSecurity配置
继承WebSecurityConfigurerAdapter启动注解@EnableWebSecurity 打开security安全配置
package com.macro.mall.auth.config; import org.aspectj.weaver.ast.And; import org.springframework.boot.actuate.autoconfigure.security.servlet.EndpointRequest; 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.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; /** * SpringSecurity配置 * Created by macro on 2020/6/19. */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 包含所有执行器端点的匹配器。它还包括链接端点,该端点位于执行器端点的基本路径上 // 匹配"/rsa/publicKey"和"/v2/api-docs"规则的放行其他的都需要通过身份验证 http.authorizeRequests() .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll() .antMatchers("/rsa/publicKey").permitAll() .antMatchers("/v2/api-docs").permitAll() .anyRequest().authenticated(); // 授权码模式必须配置 http.httpBasic(); http.httpBasic(); } @Bean @Override /** * 身份验证管理器 */ public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean /** 密码编码器 */ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
AuthorizationServerTokenServices设置令牌策略
也可以直接在ClientDetailsServiceConfigurer 里面设置
认证服务器配置继承AuthorizationServerConfigurerAdapter 启动注解@EnableAuthorizationServer 打开授权服务器
package com.macro.mall.auth.config; import com.macro.mall.auth.component.JwtTokenEnhancer; import com.macro.mall.auth.component.WhitelabelApprovalEndopintHandler; import com.macro.mall.auth.service.impl.UserServiceImpl; import lombok.AllArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.PasswordEncoder; 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.code.AuthorizationCodeServices; import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices; import org.springframework.security.oauth2.provider.token.*; import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.rsa.crypto.KeyStoreKeyFactory; import javax.xml.ws.Service; import java.security.KeyPair; import java.util.ArrayList; import java.util.List; /** * 认证服务器配置 * Created by macro on 2020/6/19. */ @AllArgsConstructor @Configuration @EnableAuthorizationServer public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter { private final PasswordEncoder passwordEncoder; private final UserServiceImpl userDetailsService; private final AuthenticationManager authenticationManager; private final JwtTokenEnhancer jwtTokenEnhancer; private final ClientDetailsService clientDetailsService; @Bean public AuthorizationCodeServices AuthorizationCodeServices(){ return new InMemoryAuthorizationCodeServices(); } @Bean public WhitelabelApprovalEndopintHandler whitelabelApprovalEndopintHandler(){ return new WhitelabelApprovalEndopintHandler(); } @Bean // 令牌存储 public TokenStore tokenStore(){ return new InMemoryTokenStore(); } /* @Bean public AuthorizationServerTokenServices tokenServices(){ DefaultTokenServices tokenServices=new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); // 客户端配置策略 tokenServices.setClientDetailsService(clientDetailsService); // 支持令牌的刷新 tokenServices.setSupportRefreshToken(true); return tokenServices; }*/ /** * 客户端详情配置 * 装载Endpoints所有相关的类配置(AuthorizationServer、TokenServices、TokenStore、ClientDetailsService、UserDetailsService)。 * http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&redirect_uri=https://www.baidu.com&scope=all * 授权码模式 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("admin-app") // 加盐 .secret(passwordEncoder.encode("123456")) //权限 .scopes("all","username") // 配置 authorization code grant type 配置多个授权模式 .authorizedGrantTypes("password", "refresh_token","authorization_code") //token有效期 .accessTokenValiditySeconds(3600*24) //刷新token有效期 .refreshTokenValiditySeconds(3600*24*7) .autoApprove(false) .redirectUris("http://localhost:8201/callback") .and() .withClient("portal-app") .secret(passwordEncoder.encode("123456"))// 加盐 .scopes("all","username") .authorizedGrantTypes("password", "refresh_token") .accessTokenValiditySeconds(3600*24) .refreshTokenValiditySeconds(3600*24*7); } @Override //令牌端点服务配置 public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // 令牌增强器链 TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> delegates = new ArrayList<>(); delegates.add(jwtTokenEnhancer); delegates.add(accessTokenConverter()); enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器 endpoints.authenticationManager(authenticationManager) .userDetailsService(userDetailsService) //配置加载用户信息的服务 .accessTokenConverter(accessTokenConverter()) .authorizationCodeServices(AuthorizationCodeServices()) .tokenStore(tokenStore()) // .tokenServices(tokenServices()) // .pathMapping("/oauth/authorize","/mall-auth/oauth/authorize") .tokenEnhancer(enhancerChain); } @Override // 允许客户端进行表单验证 令牌端点安全约束配置 public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security. // 允许客户端进行表单验证client_id和client_secret做登录认证 allowFormAuthenticationForClients(); } @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); jwtAccessTokenConverter.setKeyPair(keyPair()); //设置公钥 return jwtAccessTokenConverter; } @Bean public KeyPair keyPair() { //从classpath下的证书中获取秘钥对 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray()); return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray()); } }
授权controller,我这里因为是用的微服务架构 getway里面 会默认带一个server_name 会导致授权码模式 不能正常跳转到指定的URI上面
比如请求地址是http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&scope=all
授权成功后
会直接跳转到http://localhost:8201:/oauth/authorize
所以我自己定义了一个授权方法 和底层WhitelabelApprovalEndpoint写的差不多 只不过我把下面这段代码注释了
你们如果没有用到getway可以 直接使用默认的不需要重写 /confirm_access 授权请求
package com.macro.mall.auth.controller; import com.macro.mall.auth.component.WhitelabelApprovalEndopintHandler; import com.macro.mall.auth.domain.Oauth2TokenDto; import com.macro.mall.common.api.CommonResult; import com.macro.mall.common.constant.AuthConstant; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiImplicitParams; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint; import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint; import org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint; import org.springframework.stereotype.Controller; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import springfox.documentation.annotations.ApiIgnore; import javax.servlet.http.HttpServletRequest; import java.security.Principal; import java.util.Map; /** * 自定义Oauth2获取令牌接口 * Created by macro on 2020/7/17. */ @Controller @Api(tags = "AuthController", description = "认证中心登录认证") @RequestMapping("/oauth") @SessionAttributes("authorizationRequest") public class AuthController { @Autowired // 令牌处理器 private TokenEndpoint tokenEndpoint; @Autowired // 授权处理器 private AuthorizationEndpoint authEndpoint; @Autowired WhitelabelApprovalEndopintHandler whitelabelApprovalEndopintHandler; @ApiOperation("Oauth2获取token") @ApiImplicitParams({ @ApiImplicitParam(name = "grant_type", value = "授权模式", required = true), @ApiImplicitParam(name = "client_id", value = "Oauth2客户端ID", required = true), @ApiImplicitParam(name = "client_secret", value = "Oauth2客户端秘钥", required = true), @ApiImplicitParam(name = "refresh_token", value = "刷新token"), @ApiImplicitParam(name = "username", value = "登录用户名"), @ApiImplicitParam(name = "password", value = "登录密码") }) @RequestMapping(value = "/token", method = RequestMethod.POST) @ResponseBody public CommonResult<Oauth2TokenDto> postAccessToken(@ApiIgnore Principal principal, @ApiIgnore @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { OAuth2AccessToken oAuth2AccessToken = tokenEndpoint.postAccessToken(principal,parameters).getBody(); Oauth2TokenDto oauth2TokenDto = Oauth2TokenDto.builder() .token(oAuth2AccessToken.getValue()) .refreshToken(oAuth2AccessToken.getRefreshToken().getValue()) .expiresIn(oAuth2AccessToken.getExpiresIn()) .tokenHead(AuthConstant.JWT_TOKEN_PREFIX).build(); return CommonResult.success(oauth2TokenDto); } /** * 授权端点 * @param model * @param request * @return * @throws Exception */ @RequestMapping("/confirm_access") public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception{ return whitelabelApprovalEndopintHandler.getAccessConfirmation(model,request); } }
重写WhitelabelApprovalEndopint 授权处理器
package com.macro.mall.auth.component; import org.springframework.security.oauth2.provider.AuthorizationRequest; import org.springframework.security.web.csrf.CsrfToken; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; import org.springframework.web.util.HtmlUtils; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; /** * @author Xuyijun * @classname WhitelabelApprovalEndopintHandler.java * 授权处理器 * @create 2022-02-17, 星期四, 13:38:29 */ public class WhitelabelApprovalEndopintHandler { public ModelAndView getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception { final String approvalContent = createTemplate(model, request); if (request.getAttribute("_csrf") != null) { model.put("_csrf", request.getAttribute("_csrf")); } View approvalView = new View() { @Override public String getContentType() { return "text/html"; } @Override public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { response.setContentType(getContentType()); response.getWriter().append(approvalContent); } }; return new ModelAndView(approvalView, model); } protected String createTemplate(Map<String, Object> model, HttpServletRequest request) { AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest"); String clientId = authorizationRequest.getClientId(); StringBuilder builder = new StringBuilder(); builder.append("<html><body><h1>OAuth Approval</h1>"); builder.append("<p>Do you authorize \"").append(HtmlUtils.htmlEscape(clientId)); builder.append("\" to access your protected resources?</p>"); builder.append("<form id=\"confirmationForm\" name=\"confirmationForm\" action=\""); /*String requestPath = ServletUriComponentsBuilder.fromContextPath(request).build().getPath(); if (requestPath == null) { requestPath = ""; }*/ String requestPath="/mall-auth"; builder.append(requestPath).append("/oauth/authorize\" method=\"post\">"); builder.append("<input name=\"user_oauth_approval\" value=\"true\" type=\"hidden\"/>"); String csrfTemplate = null; CsrfToken csrfToken = (CsrfToken) (model.containsKey("_csrf") ? model.get("_csrf") : request.getAttribute("_csrf")); if (csrfToken != null) { csrfTemplate = "<input type=\"hidden\" name=\"" + HtmlUtils.htmlEscape(csrfToken.getParameterName()) + "\" value=\"" + HtmlUtils.htmlEscape(csrfToken.getToken()) + "\" />"; } if (csrfTemplate != null) { builder.append(csrfTemplate); } String authorizeInputTemplate = "<label><input name=\"authorize\" value=\"Authorize\" type=\"submit\"/></label></form>"; if (model.containsKey("scopes") || request.getAttribute("scopes") != null) { builder.append(createScopes(model, request)); builder.append(authorizeInputTemplate); } else { builder.append(authorizeInputTemplate); builder.append("<form id=\"denialForm\" name=\"denialForm\" action=\""); builder.append(requestPath).append("/oauth/authorize\" method=\"post\">"); builder.append("<input name=\"user_oauth_approval\" value=\"false\" type=\"hidden\"/>"); if (csrfTemplate != null) { builder.append(csrfTemplate); } builder.append("<label><input name=\"deny\" value=\"Deny\" type=\"submit\"/></label></form>"); } builder.append("</body></html>"); return builder.toString(); } private CharSequence createScopes(Map<String, Object> model, HttpServletRequest request) { StringBuilder builder = new StringBuilder("<ul>"); @SuppressWarnings("unchecked") Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")); for (String scope : scopes.keySet()) { String approved = "true".equals(scopes.get(scope)) ? " checked" : ""; String denied = !"true".equals(scopes.get(scope)) ? " checked" : ""; scope = HtmlUtils.htmlEscape(scope); builder.append("<li><div class=\"form-group\">"); builder.append(scope).append(": <input type=\"radio\" name=\""); builder.append(scope).append("\" value=\"true\"").append(approved).append(">Approve</input> "); builder.append("<input type=\"radio\" name=\"").append(scope).append("\" value=\"false\""); builder.append(denied).append(">Deny</input></div></li>"); } builder.append("</ul>"); return builder.toString(); } }
资源服务器我是在getway做的 这里就不写了
http://localhost:8201/mall-auth/oauth/authorize?response_type=code&client_id=admin-app&scope=all
访问
因为我配置了多个 ,选一个 点击授权即可
然后可以看到后面code
打开postman
http://localhost:8201/mall-auth/oauth/token?grant_type=authorization_code&code=Sq4mTN&redirect_uri=http://localhost:8201/callback&scope=all&client_id=admin-app&client_secret=123456
post请求
返回成功
参考链接
Oauth2认证流程官方文档https://tools.ietf.org/html/rfc6749#section-1.3
spring security 整合oauth2
oauth自定义登录页面和授权页面
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。