更新文档

This commit is contained in:
chendt 2023-12-04 09:24:34 +08:00
parent 400f63682a
commit 39505e5c99
3 changed files with 1 additions and 376 deletions

View File

@ -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)

View File

@ -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<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> 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);
}
}
```

View File

@ -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;
}
}
```