From 39505e5c99769da06e5ade3b85dbe3427f1065ff Mon Sep 17 00:00:00 2001 From: chendt <18902722133@163.com> Date: Mon, 4 Dec 2023 09:24:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/README.md | 1 + doc/认证与授权/从授权开始看源码.md | 251 -------------------- doc/认证与授权/自己写个授权的方法-开源版.md | 125 ---------- 3 files changed, 1 insertion(+), 376 deletions(-) delete mode 100644 doc/认证与授权/从授权开始看源码.md delete mode 100644 doc/认证与授权/自己写个授权的方法-开源版.md diff --git a/doc/README.md b/doc/README.md index 7548c99..e7551e7 100644 --- a/doc/README.md +++ b/doc/README.md @@ -42,6 +42,7 @@ ### 1.2 安装jdk + mysql + redis + maven 如果不了解怎么安装jdk的,可以参考 [菜鸟教程的java相关](https://www.runoob.com/java/java-environment-setup.html) +- 教程展示的是oracle,需要自行搜索openjdk的下载链接,下载jdk17版本 如果不了解怎么安装mysql的,可以参考 [菜鸟教程的mysql相关](https://www.runoob.com/mysql/mysql-install.html) diff --git a/doc/认证与授权/从授权开始看源码.md b/doc/认证与授权/从授权开始看源码.md deleted file mode 100644 index e8026a2..0000000 --- a/doc/认证与授权/从授权开始看源码.md +++ /dev/null @@ -1,251 +0,0 @@ -> 如果不理解oauth协议的推荐阅读 阮一峰的[理解OAuth 2.0](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html) - -当然,我们也要简单介绍下oauth的运行流程: - -``` - +--------+ +---------------+ - | |--(A)- Authorization Request ->| Resource | - | | | Owner | - | |<-(B)-- Authorization Grant ---| | - | | +---------------+ - | | - | | +---------------+ - | |--(C)-- Authorization Grant -->| Authorization | - | Client | | Server | - | |<-(D)----- Access Token -------| | - | | +---------------+ - | | - | | +---------------+ - | |--(E)----- Access Token ------>| Resource | - | | | Server | - | |<-(F)--- Protected Resource ---| | - +--------+ +---------------+ -``` - -运行流程如下图,摘自RFC 6749。 - - - -- (A)用户打开客户端以后,客户端要求用户给予授权。 -- (B)用户同意给予客户端授权。 -- (C)客户端使用上一步获得的授权,向认证服务器申请令牌。 -- (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。 -- (E)客户端使用令牌,向资源服务器申请获取资源。 -- (F)资源服务器确认令牌无误,同意向客户端开放资源。 - - - -我们是对内的系统,并不需要那么复杂的流程,所以我们看下oauth的授权模式当中的密码模式: - -``` - +----------+ - | Resource | - | Owner | - | | - +----------+ - v - | Resource Owner - (A) Password Credentials - | - v - +---------+ +---------------+ - | |>--(B)---- Resource Owner ------->| | - | | Password Credentials | Authorization | - | Client | | Server | - | |<--(C)---- Access Token ---------<| | - | | (w/ Optional Refresh Token) | | - +---------+ +---------------+ -``` - -这里的流程相对就比较简单了: - -(A)用户向客户端提供用户名和密码。 - -(B)客户端将用户名和密码发给认证服务器,向后者请求令牌。 - -(C)认证服务器确认无误后,向客户端提供访问令牌。 - - - -现在将简单的转换下思路: - -- `Resource Owner`:资源拥有者,拥有订单,购物车等数据的人,既用户 - -- `Client`:客户端,浏览器 - -- `Authorization Server`:认证服务器,也就是服务器咯。 - - - -在此A、B、C三个流程就变成了: - -(A)用户在浏览器输入用户名和密码。 - -(B)浏览器将用户名和密码发给服务器,向后者请求令牌(token)。 - -(C)服务器确认无误后,返回token给用户。 - - - -但是根据标准的流程,并没有验证码之类的容身之地。而`spring security oauth2` 给我们提供的只能是标准的流程,所以我们对代码进行一些适配,能够适应我们自己的需求。 - - - -## spring的部分源码 - -我们先来看下`spring security oauth2`的部分源码 - -首先我们直接进行授权的时候,调用的url大概为:`http://localhost:8080/oauth/token?username=user_1&password=123456&grant_type=password&scope=select&client_id=client_2&client_secret=123456`,那么授权肯定是与该链接相关联的。基于这个猜测,我们去寻找源码吧。 - - - -在`idea`中使用全局搜索,搜索 字符串`"/oauth/token"`(带着引号),发现了一个类,似乎与这个请求有关 `ClientCredentialsTokenEndpointFilter` - -```java -public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter { - - public ClientCredentialsTokenEndpointFilter() { - this("/oauth/token"); - } -} -``` - -``` -ClientCredentialsTokenEndpointFilter - ---> AbstractAuthenticationProcessingFilter - ---> GenericFilterBean - ---> Filter -``` - -发现,这个类是一个 `Filter` 也就是过滤器,通过这个过滤器,过滤请求,那么,我们去看看`doFilter`方法咯,`doFilter` 在 `ClientCredentialsTokenEndpointFilter` 的父类 `AbstractAuthenticationProcessingFilter` 上。 - -我们看看`AbstractAuthenticationProcessingFilter`: - -```java - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) - throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - // 如果不是认证的请求,直接下一个filter - // 这里是怎么判断是否是下一个请求呢? - // 答:看看url是不是上面ClientCredentialsTokenEndpointFilter 创建时传过来的url,也就是 /oauth/token - if (!requiresAuthentication(request, response)) { - chain.doFilter(request, response); - return; - } - - Authentication authResult; - try { - // 调用attemptAuthentication 方法,返回一个 Authentication 的实现类,也就是认证信息,这个实现类非常重要!!! - authResult = attemptAuthentication(request, response); - // 如果找不到,那就没了 - if (authResult == null) { - return; - } - } - // 调用成功的方法 - successfulAuthentication(request, response, chain, authResult); - } -``` - -这里最重要的方法`attemptAuthentication` 生成一个授权信息,能够返回,则证明登录已经成功了,所以真正的登录与这里有关。 - -我们回到`ClientCredentialsTokenEndpointFilter` 这个实现类里面看看`attemptAuthentication`方法吧 - -```java - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) - throws AuthenticationException, IOException, ServletException { - - // ======精简没啥用的方法======== - - // 构造一个UsernamePasswordAuthenticationToken - UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, - clientSecret); - // 调用认证方法进行认证 - return this.getAuthenticationManager().authenticate(authRequest); - - } -``` - -我们通过添加断点可以发现 `this.getAuthenticationManager()` 是一个`ProviderManager` 对象,我们看下 - -`this.getAuthenticationManager().authenticate()` 里面的 `authenticate` - -```java -public class ProviderManager{ - public Authentication authenticate(Authentication authentication) - throws AuthenticationException { - - Authentication result = null; - - for (AuthenticationProvider provider : getProviders()) { - // 在一堆的provider中寻找到一个合适的授权提供者 - if (!provider.supports(toTest)) { - continue; - } - // 由授权提供者进行授权 - result = provider.authenticate(authentication); - } - if (result != null) { - return result; - } - } -} -``` - -一路追踪到这里,我们发现,实际上,是通过`provider.supports(toTest)` 寻找一个合适的授权提供者,使用`provider.authenticate(authentication)`就行授权,而`supports` 的依据是通过之前生成的token来判断是否支持: - -```java - public boolean supports(Class authentication) { - return (UsernamePasswordAuthenticationToken.class - .isAssignableFrom(authentication)); - } -``` - - - -我们整理下这几个流程 - -``` -ClientCredentialsTokenEndpointFilter.doFilter() - --> AbstractAuthenticationProcessingFilter.attemptAuthentication() - --> ProviderManager.authenticate() - --> AuthenticationProvider.supports() - --> AuthenticationProvider.authenticate() -``` - - - -我们可以看到这里主要就是干了几件事情 - -- 通过filter 确定登录要过滤的url -- 通过filter 确定生成的`AbstractAuthenticationToken` 比如 `UsernamePasswordAuthenticationToken` -- 通过生成的`AbstractAuthenticationToken` 确定`AuthenticationProvider` -- 通过`AuthenticationProvider` 最后调用 `authenticate()`方法最后进行授权 - - - -最后通过`RequestMapping` 返回 - -```java -@FrameworkEndpoint -public class TokenEndpoint extends AbstractEndpoint{ - @RequestMapping(value = "/oauth/token", method=RequestMethod.POST) - public ServerResponseEntity postAccessToken(Principal principal, @RequestParam - Map parameters) throws HttpRequestMethodNotSupportedException { - - - String clientId = getClientId(principal); - ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId); - - TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient); - - OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); - - return getResponse(token); - } -} -``` - diff --git a/doc/认证与授权/自己写个授权的方法-开源版.md b/doc/认证与授权/自己写个授权的方法-开源版.md deleted file mode 100644 index a8770ac..0000000 --- a/doc/认证与授权/自己写个授权的方法-开源版.md +++ /dev/null @@ -1,125 +0,0 @@ -通过【从授权开始看源码】我们可以看到这里主要就是干了几件事情 - -- 通过filter 确定登录要过滤的url -- 通过filter 确定生成的`AbstractAuthenticationToken` 比如 `UsernamePasswordAuthenticationToken` -- 通过生成的`AbstractAuthenticationToken` 确定`AuthenticationProvider` -- 通过`AuthenticationProvider` 最后调用 `authenticate()`方法最后进行授权 - - - -根据上面我们对自己对代码进行了一些封装 - -我们先来看`LoginAuthenticationFilter` - -```java -public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { - - private AuthenticationTokenParser authenticationTokenParser; - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - - AbstractAuthenticationToken authRequest = authenticationTokenParser.parse(requestBody); - - return this.getAuthenticationManager().authenticate(authRequest); - } - - - public void setAuthenticationTokenParser(AuthenticationTokenParser authenticationTokenParser) { - this.authenticationTokenParser = authenticationTokenParser; - } -} -``` - -这里的登录继承了`UsernamePasswordAuthenticationFilter` 里面写了 - -```java -public UsernamePasswordAuthenticationFilter() { - super(new AntPathRequestMatcher("/login", "POST")); -} -``` - -这就是为什么登录的接口是`/login`的原因 - - - -我们再来看看生成`AbstractAuthenticationToken `的方法 - -`AbstractAuthenticationToken authRequest = authenticationTokenParser.parse(requestBody);` - -这里决定了生成什么token,将会决定后面的`AuthenticationProvider` - - - -我们先来看`AdminAuthenticationProvider` - -``` - @Override - public boolean supports(Class authentication) { - return AdminAuthenticationToken.class.isAssignableFrom(authentication); - } -``` - -这里决定`AdminAuthenticationToken` 是通过`AdminAuthenticationProvider` 进行校验 - - - -再来看下完整的`AdminAuthenticationProvider` 你就知道验证码在哪里校验的了,是不是很简单 - -```java -public class AdminAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { - - private final YamiUserDetailsService yamiUserDetailsService; - - private final PasswordEncoder passwordEncoder; - - @Override - protected UserDetails retrieveUser(String username, Authentication authentication) throws BaseYamiAuth2Exception { - UserDetails user; - try { - user = yamiUserDetailsService.loadUserByUsername(username); - } catch (UsernameNotFoundExceptionBase var6) { - throw new UsernameNotFoundExceptionBase("账号或密码不正确"); - } - if (!user.isEnabled()) { - throw new UsernameNotFoundExceptionBase("账号已被锁定,请联系管理员"); - } - return user; - } - - @Override - protected void additionalAuthenticationChecks(UserDetails sysUser, Authentication authentication) throws BaseYamiAuth2Exception { - AdminAuthenticationToken adminAuthenticationToken = (AdminAuthenticationToken) authentication; - - String kaptchaKey = SecurityConstants.SPRING_SECURITY_RESTFUL_IMAGE_CODE + adminAuthenticationToken.getSessionUUID(); - - String kaptcha = RedisUtil.get(kaptchaKey); - - RedisUtil.del(kaptchaKey); - - if(StrUtil.isBlank(adminAuthenticationToken.getImageCode()) || !adminAuthenticationToken.getImageCode().equalsIgnoreCase(kaptcha)){ - throw new ImageCodeNotMatchExceptionBase("验证码有误"); - } - - - - String encodedPassword = sysUser.getPassword(); - String rawPassword = authentication.getCredentials().toString(); - - // 密码不正确 - if (!passwordEncoder.matches(rawPassword,encodedPassword)){ - throw new BadCredentialsExceptionBase("账号或密码不正确"); - } - } - - @Override - protected Authentication createSuccessAuthentication(Authentication authentication, UserDetails user) { - AdminAuthenticationToken result = new AdminAuthenticationToken(user, authentication.getCredentials()); - result.setDetails(authentication.getDetails()); - return result; - } - -} - -``` -