# SpringBootExperiment04
**Repository Path**: bothin/SpringBootExperiment04
## Basic Information
- **Project Name**: SpringBootExperiment04
- **Description**: 【Spring Boot实验】实验四 基于Spring Security码云OAuth2认证的实验仓库
- **Primary Language**: Java
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2020-05-15
- **Last Updated**: 2021-11-24
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
### 一、 实验目的
1. 掌握使用Spring Security框架;
2. 掌握配置Spring Security的安全过滤链;
3. 掌握编写Spring Security单元测试;
4. 掌握创建接入码云的应用;
5. 掌握码云OAuth2认证基本流程;
6. 掌握使用码云API;
7. 了解使用模板引擎或前端框架制作用户登录界面。
### 二、 实验环境
1. JDK 1.8或更高版本
2. Maven 3.6+
3. IntelliJ IDEA
### 三、 实验任务
#### 1、 登录码云,fork实验四的作业仓库。
仓库地址:https://gitee.com/dgut-sai/spring-security-gitee-experiment-4
#### 2、 根据下面的步骤填充代码,运行并测试成功:
##### 1) 步骤一:创建接入码云的应用。
码云参考文档:https://gitee.com/api/v5/oauth_doc#/list-item-3

##### 2) 步骤二:编写重定向过滤器的业务逻辑。
当用户访问/oauth2/gitee时,本重定向过滤器拦截请求,并将用户重定向到码云三方认证页面上。
```java
// 应用通过 浏览器 或 Webview 将用户引导到码云三方认证页面上( GET请求 )
// 重定向地址:https://gitee.com/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code
//////////////////////////////////////////////////////////////
/// 步骤二:编写重定向过滤器的业务逻辑。
/// 当用户访问/oauth2/gitee时,本重定向过滤器拦截请求,并将用户重定向到码云三方认证页面上。
//////////////////////////////////////////////////////////////
String request_url = UriComponentsBuilder.fromUriString(AUTHORIZE_URL)
.buildAndExpand(CLIENT_ID, REDIRECT_URI).toString();
response.sendRedirect(request_url);
```
##### 3) 步骤三:使用码云access_token API向码云认证服务器发送post请求获取access_token。
```java
/**
* 获取码云API的访问令牌access_token
*
* @see UriComponentsBuilder
* @see UriComponentsBuilder#buildAndExpand(Object...)
* @see RequestEntity
* @see RestTemplate#exchange(RequestEntity, Class)
* @see JacksonJsonParser
*
*/
private String getAccessToken(String code) {
////////////////////////////////////////////////////
/// 步骤三:使用码云access_token API向码云认证服务器发送post请求获取access_token。
String request_url = UriComponentsBuilder.fromUriString(ACCESS_TOKEN_API_URI)
.buildAndExpand(code, CLIENT_ID, REDIRECT_URI, CLIENT_SECRET).toString();
RequestEntity requestEntity = RequestEntity.post(URI.create(request_url))
.headers(httpHeaders -> httpHeaders.add("User-Agent", "Mozilla/5.0"))
.build();
ResponseEntity responseEntity = REST.exchange(requestEntity, String.class);
String json = responseEntity.getBody();
LOGGER.info("【access_token的json字符串】:json={}",json);
Map parseMap = new JacksonJsonParser(new ObjectMapper()).parseMap(json);
return (String) parseMap.get("access_token");
////////////////////////////////////////////////////
// 正确返回的access_token的json字符串:
// {"access_token":"7282a1140867f6e3527f805af1950ea8","token_type":"bearer","expires_in":86400,"refresh_token":"0664cd3b66e36943b341285764a257ccfc7265a319dfcdd93c5f1bfbd4e023f1","scope":"user_info","created_at":1589124246}
}
```
##### 4) 步骤四:使用码云API获取授权用户的资料。
码云参考文档:https://gitee.com/api/v5/swagger#/getV5User
```java
/**
* 获取码云授权用户的信息
*
* @see RequestEntity
* @see RestTemplate#exchange(RequestEntity, Class)
* @see JacksonJsonParser
*/
private Map getUserInfo(String accessToken) {
////////////////////////////////////////////////////
/// 步骤四:使用码云API获取授权用户的资料。
/// 参考:https://gitee.com/api/v5/swagger#/getV5User
String request_url = UriComponentsBuilder.fromUriString(USER_INFO_URI)
.buildAndExpand(accessToken).toString();
RequestEntity requestEntity = RequestEntity.get(URI.create(request_url))
.headers(httpHeaders -> {
httpHeaders.add("User-Agent", "Mozilla/5.0");
})
.build();
ResponseEntity responseEntity = REST.exchange(requestEntity, String.class);
String json = responseEntity.getBody();
LOGGER.info("【User_info的json字符串】:json={}",json);
return new JacksonJsonParser(new ObjectMapper()).parseMap(json);
////////////////////////////////////////////////////
}
/**
* 认证成功后,重新构造Authentication。
*/
private Authentication createSuccessAuthentication(Map userInfo, HttpServletRequest request) {
// 构造 UserDetails 自定义CustomUserDetail
CustomUserDetail user = new CustomUserDetail(userInfo.get("login").toString(), "",
AuthorityUtils.createAuthorityList("USER"),userInfo);
GiteeOAuth2LoginAuthenticationToken authenticationToken = new GiteeOAuth2LoginAuthenticationToken(user, AuthorityUtils.createAuthorityList("USER"));
// 设置认证用户的额外信息,比如 IP 地址、经纬度等。下面代码将赋值一个WebAuthenticationDetails对象,它的构造函数是request,会封装HttpServletRequest的信息。
AuthenticationDetailsSource authenticationDetailsSource = new WebAuthenticationDetailsSource();
authenticationToken.setDetails(authenticationDetailsSource.buildDetails(request));
return authenticationToken;
}
```
##### 5) 步骤五:把自定义的两个Filter加进安全过滤链。
注意:不要加在SecurityContextPersistenceFilter前面。
```java
////////////////////////////////////////////////////////////////
/// 步骤五:把自定义的两个Filter加进安全过滤链
/// 注意:不要加在SecurityContextPersistenceFilter前面就行。
////////////////////////////////////////////////////////////////
http.addFilterAfter(postProcess(new GiteeOAuth2RedirectFilter()), HeaderWriterFilter.class)
.addFilterAfter(postProcess(new GiteeOAuth2LoginAuthenticationFilter()),HeaderWriterFilter.class);
```
##### 6) 步骤六:把我们自定义的SecurityConfigurer应用到安全过滤链。

##### 7) 步骤七:改造/user接口,返回码云用户资料给前端;改造user.ftlh模板用于显示用户资料。
```java
/**
* 获取登录用户的资料接口
*
* @see Authentication
* @see Principal
* @see Model
*/
@GetMapping("/user")
String userIndex(Model model) {
////////////////////////////////////
/// 步骤七:改造/user接口,返回码云用户资料给前端;改造user.ftlh模板用于显示用户资料。
////////////////////////////////////
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetail customUserDetail = (CustomUserDetail) authentication.getPrincipal();
model.addAttribute("userInfo",customUserDetail.getUserInfo());
return "user";
}
```


##### 8) 步骤八:编写单元测试。模拟一个登录用户,访问受保护的接口/test,断言接口的返回内容body部分是否一致。

```java
/**
* 测试限权接口
*
* 模拟一个登录用户,访问受保护的接口。
*
* @see WithMockUser
* @see MockMvc#perform(RequestBuilder)
* @see MockMvcRequestBuilders#get(String, Object...)
*/
@Test
@WithMockUser(username = "user",authorities = {"USER"})
public void test() throws Exception {
////////////////////////////////////////////
/// 步骤八:模拟一个登录用户,访问受保护的接口/test,断言接口的返回内容body部分是否一致。
////////////////////////////////////////////
mvc.perform(MockMvcRequestBuilders.get("/test"))
.andExpect(status().isOk())
.andExpect(content().string("访问/test接口成功,你拥有USER权限"));
}
@Test
public void testFormLogin() throws Exception {
// 测试登录成功
mvc.perform(formLogin("/login").user("user").password("user"))
.andExpect(unauthenticated());
}
@Test
public void testFormLoginFail() throws Exception {
// 测试登录失败
mvc
.perform(formLogin("/login").user("admin").password("invalid"))
.andExpect(unauthenticated());
}
@Test
public void testLogoutFail() throws Exception {
// 测试退出登录
mvc.perform(logout("/logout")).andExpect(unauthenticated());
}
```