From ea71002ed6e77fb0e58baa3ba5e9dc7ebd889585 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 1 Oct 2022 20:30:15 +0800 Subject: [PATCH] 完成 yudao-sso-demo-by-code 实现修改用户的信息 --- yudao-example/yudao-sso-demo-by-code/pom.xml | 6 ++++++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java | 2 ++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java | 27 +++++++++++++++++++++++++-- yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java | 35 +++++++++++++++++++++++++++++++++++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java | 17 ++++++++++++++--- yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java | 22 +++++++++++++--------- yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/SecurityUtils.java | 102 ------------------------------------------------------------------------------------------------------ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java | 64 ---------------------------------------------------------------- yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java | 44 ++++++++++++++++++++++++++++++++++++++++++++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java | 36 ++++++++++++++++++++++++++++++++++++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java | 28 ++++++++++++++++++++++++++++ yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html | 29 ++++++++++++++++++++++++++++- 14 files changed, 400 insertions(+), 181 deletions(-) create mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java delete mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/SecurityUtils.java delete mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java create mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java create mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java create mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java create mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java create mode 100644 yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java diff --git a/yudao-example/yudao-sso-demo-by-code/pom.xml b/yudao-example/yudao-sso-demo-by-code/pom.xml index 3ea138a..9b003c4 100644 --- a/yudao-example/yudao-sso-demo-by-code/pom.xml +++ b/yudao-example/yudao-sso-demo-by-code/pom.xml @@ -50,6 +50,12 @@ + cn.hutool + hutool-all + 5.8.5 + + + org.projectlombok lombok true diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java index f29da71..bdb417c 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/OAuth2Client.java @@ -16,6 +16,8 @@ import java.nio.charset.StandardCharsets; /** * OAuth 2.0 客户端 + * + * 对应调用 OAuth2OpenController 接口 */ @Component public class OAuth2Client { diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java index 8b2fa2a..666bd3e 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/UserClient.java @@ -2,8 +2,9 @@ package cn.iocoder.yudao.ssodemo.client; import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; -import cn.iocoder.yudao.ssodemo.framework.core.SecurityUtils; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.*; import org.springframework.stereotype.Component; @@ -13,7 +14,9 @@ import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; /** - * OAuth 2.0 客户端 + * 用户 User 信息的客户端 + * + * 对应调用 OAuth2UserController 接口 */ @Component public class UserClient { @@ -42,6 +45,26 @@ public class UserClient { return exchange.getBody(); } + public CommonResult updateUser(UserUpdateReqDTO updateReqDTO) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + // 使用 updateReqDTO 即可 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + private static void addTokenHeader(HttpHeaders headers) { LoginUser loginUser = SecurityUtils.getLoginUser(); Assert.notNull(loginUser, "登录用户不能为空"); diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 0000000..e711d73 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/client/dto/user/UserUpdateReqDTO.java @@ -0,0 +1,35 @@ +package cn.iocoder.yudao.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 更新用户基本信息 Request DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserUpdateReqDTO { + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java index 5626b71..819c752 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/controller/UserController.java @@ -3,9 +3,8 @@ package cn.iocoder.yudao.ssodemo.controller; import cn.iocoder.yudao.ssodemo.client.UserClient; import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; import cn.iocoder.yudao.ssodemo.client.dto.user.UserInfoRespDTO; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import cn.iocoder.yudao.ssodemo.client.dto.user.UserUpdateReqDTO; +import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; @@ -26,4 +25,16 @@ public class UserController { return userClient.getUser(); } + /** + * 更新当前登录用户的昵称 + * + * @param nickname 昵称 + * @return 成功 + */ + @PutMapping("/update") + public CommonResult updateUser(@RequestParam("nickname") String nickname) { + UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); + return userClient.updateUser(updateReqDTO); + } + } diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java index 3a27f01..5c8a4e4 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/config/SecurityConfiguration.java @@ -1,10 +1,12 @@ package cn.iocoder.yudao.ssodemo.framework.config; -import cn.iocoder.yudao.ssodemo.framework.core.TokenAuthenticationFilter; +import cn.iocoder.yudao.ssodemo.framework.core.filter.TokenAuthenticationFilter; +import cn.iocoder.yudao.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.annotation.Resource; @@ -12,17 +14,14 @@ import javax.annotation.Resource; @Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { -// /** -// * Token 认证过滤器 Bean -// */ -// @Bean -// public TokenAuthenticationFilter authenticationTokenFilter(OAuth2Client oauth2Client) { -// return new TokenAuthenticationFilter(oauth2Client); -// } - @Resource private TokenAuthenticationFilter tokenAuthenticationFilter; + @Resource + private AccessDeniedHandlerImpl accessDeniedHandler; + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + @Override protected void configure(HttpSecurity httpSecurity) throws Exception { // 设置 URL 安全权限 @@ -36,7 +35,12 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .and().authorizeRequests() .anyRequest().authenticated(); + // 设置处理器 + httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); + // 添加 Token Filter httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } + } diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/SecurityUtils.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/SecurityUtils.java deleted file mode 100644 index adfd32b..0000000 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/SecurityUtils.java +++ /dev/null @@ -1,102 +0,0 @@ -package cn.iocoder.yudao.ssodemo.framework.core; - -import org.springframework.lang.Nullable; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -import org.springframework.util.StringUtils; - -import javax.servlet.http.HttpServletRequest; -import java.util.Collections; - -/** - * 安全服务工具类 - * - * @author 芋道源码 - */ -public class SecurityUtils { - - public static final String AUTHORIZATION_BEARER = "Bearer"; - - private SecurityUtils() {} - - /** - * 从请求中,获得认证 Token - * - * @param request 请求 - * @param header 认证 Token 对应的 Header 名字 - * @return 认证 Token - */ - public static String obtainAuthorization(HttpServletRequest request, String header) { - String authorization = request.getHeader(header); - if (!StringUtils.hasText(authorization)) { - return null; - } - int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); - if (index == -1) { // 未找到 - return null; - } - return authorization.substring(index + 7).trim(); - } - - /** - * 获得当前认证信息 - * - * @return 认证信息 - */ - public static Authentication getAuthentication() { - SecurityContext context = SecurityContextHolder.getContext(); - if (context == null) { - return null; - } - return context.getAuthentication(); - } - - /** - * 获取当前用户 - * - * @return 当前用户 - */ - @Nullable - public static LoginUser getLoginUser() { - Authentication authentication = getAuthentication(); - if (authentication == null) { - return null; - } - return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; - } - - /** - * 获得当前用户的编号,从上下文中 - * - * @return 用户编号 - */ - @Nullable - public static Long getLoginUserId() { - LoginUser loginUser = getLoginUser(); - return loginUser != null ? loginUser.getId() : null; - } - - /** - * 设置当前用户 - * - * @param loginUser 登录用户 - * @param request 请求 - */ - public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { - // 创建 Authentication,并设置到上下文 - Authentication authentication = buildAuthentication(loginUser, request); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { - // 创建 UsernamePasswordAuthenticationToken 对象 - UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( - loginUser, null, Collections.emptyList()); - authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - return authenticationToken; - } - -} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java deleted file mode 100644 index cb90b48..0000000 --- a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/TokenAuthenticationFilter.java +++ /dev/null @@ -1,64 +0,0 @@ -package cn.iocoder.yudao.ssodemo.framework.core; - -import cn.iocoder.yudao.ssodemo.client.OAuth2Client; -import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; -import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import org.springframework.web.filter.OncePerRequestFilter; - -import javax.annotation.Resource; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * Token 过滤器,验证 token 的有效性 - * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 - * - * @author 芋道源码 - */ -@Component -public class TokenAuthenticationFilter extends OncePerRequestFilter { - - @Resource - private OAuth2Client oauth2Client; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - // 1. 获得访问令牌 - String token = SecurityUtils.obtainAuthorization(request, "Authentication"); - if (StringUtils.hasText(token)) { - // 2. 基于 token 构建登录用户 - LoginUser loginUser = buildLoginUserByToken(token); - // 3. 设置当前用户 - if (loginUser != null) { - SecurityUtils.setLoginUser(loginUser, request); - } - } - - // 继续过滤链 - filterChain.doFilter(request, response); - } - - private LoginUser buildLoginUserByToken(String token) { - try { - CommonResult accessTokenResult = oauth2Client.checkToken(token); - OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); - if (accessToken == null) { - return null; - } - // 构建登录用户 - return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) - .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) - .setAccessToken(accessToken.getAccessToken()); - } catch (Exception exception) { - // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 - return null; - } - } - -} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..106e1bd --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,66 @@ +package cn.iocoder.yudao.ssodemo.framework.core.filter; + +import cn.iocoder.yudao.ssodemo.client.OAuth2Client; +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@Component +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Resource + private OAuth2Client oauth2Client; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // 1. 获得访问令牌 + String token = SecurityUtils.obtainAuthorization(request, "Authentication"); + if (StringUtils.hasText(token)) { + // 2. 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token); + // 3. 设置当前用户 + if (loginUser != null) { + SecurityUtils.setLoginUser(loginUser, request); + } + } + + // 继续过滤链 + filterChain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token) { + try { + CommonResult accessTokenResult = oauth2Client.checkToken(token); + OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); + if (accessToken == null) { + return null; + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) + .setAccessToken(accessToken.getAccessToken()); + } catch (Exception exception) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 0000000..dccc35e --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.ssodemo.framework.core.handler; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.framework.core.util.SecurityUtils; +import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Component +@SuppressWarnings("JavadocReference") +@Slf4j +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityUtils.getLoginUserId(), e); + // 返回 403 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..241ff56 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,36 @@ +package cn.iocoder.yudao.ssodemo.framework.core.handler; + +import cn.iocoder.yudao.ssodemo.client.dto.CommonResult; +import cn.iocoder.yudao.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java new file mode 100644 index 0000000..0352a47 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/SecurityUtils.java @@ -0,0 +1,103 @@ +package cn.iocoder.yudao.ssodemo.framework.core.util; + +import cn.iocoder.yudao.ssodemo.framework.core.LoginUser; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java new file mode 100644 index 0000000..50f1836 --- /dev/null +++ b/yudao-example/yudao-sso-demo-by-code/src/main/java/cn/iocoder/yudao/ssodemo/framework/core/util/ServletUtils.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.ssodemo.framework.core.util; + +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JSONUtil.toJsonStr(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + +} diff --git a/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html index 491982b..0f69e6b 100644 --- a/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html +++ b/yudao-example/yudao-sso-demo-by-code/src/main/resources/static/index.html @@ -20,6 +20,33 @@ + '&response_type=' + responseType; } + /** + * 修改昵称 + */ + function updateNickname() { + const nickname = prompt("请输入新的昵称", ""); + if (!nickname) { + return; + } + // 更新用户的昵称 + const accessToken = localStorage.getItem('ACCESS-TOKEN'); + $.ajax({ + url: "http://127.0.0.1:18080/user/update?nickname=" + nickname, + method: 'PUT', + headers: { + 'Authentication': 'Bearer ' + accessToken + }, + success: function (result) { + if (result.code !== 0) { + alert('更新昵称失败,原因:' + result.msg) + return; + } + alert('更新昵称成功!'); + $('#nicknameSpan').html(nickname); + } + }); + } + $(function () { const accessToken = localStorage.getItem('ACCESS-TOKEN'); // 情况一:未登录 @@ -58,7 +85,7 @@ -- libgit2 0.21.4