# 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 ![创建应用](https://images.gitee.com/uploads/images/2020/0515/104454_bfd5b610_4846724.png "1.png") ##### 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应用到安全过滤链。 ![输入图片说明](https://images.gitee.com/uploads/images/2020/0515/105038_535fdebb_4846724.png "屏幕截图.png") ##### 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"; } ``` ![授权](https://images.gitee.com/uploads/images/2020/0515/112256_5283e868_4846724.png "屏幕截图.png") ![用户详情页](https://images.gitee.com/uploads/images/2020/0515/105208_3aaf8b46_4846724.png "屏幕截图.png") ##### 8) 步骤八:编写单元测试。模拟一个登录用户,访问受保护的接口/test,断言接口的返回内容body部分是否一致。 ![test](https://images.gitee.com/uploads/images/2020/0515/105332_659b3847_4846724.png "屏幕截图.png") ```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()); } ```