文章目录
认证授权中心自定义令牌增强自定义认证端点返回结果登录逻辑调整,增强令牌返回参数测试验证用户微服务构建配置类构建相关实体类登录退出登录在之前的博客我写了SpringCloud整合spring security+ oauth2+Redis实现认证授权,本文对返回的token实现自定义增强令牌返回结果,以及对于oauth2存在Redis的数据进行解释。
认证授权中心自定义令牌增强
自定义认证端点返回结果
访问oauth/token,oauth2默认返回的授权token信息如下:
如果不自定义可以看到访问oauth/token,默认访问的是TokenEndpoint下的接口
在授权服务中自定义oauth2控制器实现自定义令牌参数返回,代码如下:
package com.zjq.oauth2.server.controller;import mons.model.domain.ResultInfo;import mons.utils.ResultInfoUtil;import org.springframework.mon.DefaultOAuth2AccessToken;import org.springframework.mon.OAuth2AccessToken;import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint;import org.springframework.web.HttpRequestMethodNotSupportedException;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;import javax.servlet.http.HttpServletRequest;import java.security.Principal;import java.util.LinkedHashMap;import java.util.Map;/*** Oauth2 控制器*/@RestController@RequestMapping("oauth")public class OAuthController {@Resourceprivate TokenEndpoint tokenEndpoint;@Resourceprivate HttpServletRequest request;@PostMapping("token")public ResultInfo postAccessToken(Principal principal, @RequestParam Map<String, String> parameters)throws HttpRequestMethodNotSupportedException {return custom(tokenEndpoint.postAccessToken(principal, parameters).getBody());}/*** 自定义 Token 返回对象** @param accessToken* @return*/private ResultInfo custom(OAuth2AccessToken accessToken) {DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;Map<String, Object> data = new LinkedHashMap(token.getAdditionalInformation());data.put("accessToken", token.getValue());data.put("expireIn", token.getExpiresIn());data.put("scopes", token.getScope());if (token.getRefreshToken() != null) {data.put("refreshToken", token.getRefreshToken().getValue());}return ResultInfoUtil.buildSuccess(request.getServletPath(), data);}}
登录逻辑调整,增强令牌返回参数
添加登录认证对象:
package mons.model.domain;import cn.hutool.core.util.StrUtil;import mon.collect.Lists;import lombok.Getter;import lombok.Setter;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.authority.AuthorityUtils;import org.springframework.security.core.authority.SimpleGrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;import java.util.List;import java.util.stream.Collectors;import java.util.stream.Stream;/*** 登录认证对象** @Author zjq* @Date /10/12*/@Getter@Setterpublic class SignInIdentity implements UserDetails {// 主键private Integer id;// 用户名private String username;// 昵称private String nickname;// 密码private String password;// 手机号private String phone;// 邮箱private String email;// 头像private String avatarUrl;// 角色private String roles;// 是否有效 0=无效 1=有效private int isValid;// 角色集合, 不能为空private List<GrantedAuthority> authorities;// 获取角色信息@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {if (StrUtil.isNotBlank(this.roles)) {// 获取数据库中的角色信息Lists.newArrayList();this.authorities = Stream.of(this.roles.split(",")).map(role -> {return new SimpleGrantedAuthority(role);}).collect(Collectors.toList());} else {// 如果角色为空则设置为 ROLE_USERthis.authorities = maSeparatedStringToAuthorityList("ROLE_USER");}return this.authorities;}@Overridepublic String getPassword() {return this.password;}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return this.isValid == 0 ? false : true;}}
登录后返回登录认证对象:
@Resourceprivate UsersMapper usersMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {AssertUtil.isNotEmpty(username, "请输入用户名");Users users = usersMapper.selectByAccountInfo(username);if (users == null) {throw new UsernameNotFoundException("用户名或密码错误,请重新输入");}// 初始化登录认证对象SignInIdentity signInIdentity = new SignInIdentity();// 拷贝属性BeanUtils.copyProperties(users, signInIdentity);return signInIdentity;}
在授权服务配置类AuthorizationServerConfiguration中增强令牌返回信息:
/*** 配置授权以及令牌的访问端点和令牌服务** @param endpoints* @throws Exception*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {// 认证器endpoints.authenticationManager(authenticationManager)// 具体登录的方法.userDetailsService(userService)// token 存储的方式:Redis.tokenStore(redisTokenStore)// 令牌增强对象,增强返回的结果.tokenEnhancer((accessToken, authentication) -> {// 获取登录用户的信息,然后设置SignInIdentity signInIdentity = (SignInIdentity) authentication.getPrincipal();LinkedHashMap<String, Object> map = new LinkedHashMap<>();map.put("nickname", signInIdentity.getNickname());map.put("avatarUrl", signInIdentity.getAvatarUrl());DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;token.setAdditionalInformation(map);return token;});}
测试验证
访问请求/oauth/token,可以看到已经返回我们自己需要的认证授权返回结果。
至此,认证授权微服务已经构建完成。
上述已经完成了认证授权中心的搭建。下面继续通过用户微服务访问认证中心实现登录退出。
接下来我们构建一个用户微服务并通过调用授权认证服务实现登录和退出。
用户微服务构建
用户服务相关pom依赖如下:
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><parent><artifactId>oauth2-demo</artifactId><groupId>com.zjq</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>ms-users</artifactId><dependencies><!-- eureka client --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><!-- spring web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- spring data redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- mybatis --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency><!-- mysql --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!-- commons 公共项目 --><dependency><groupId>com.zjq</groupId><artifactId>commons</artifactId><version>1.0-SNAPSHOT</version></dependency><!-- 自定义的元数据依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency></dependencies></project>
配置文件内容如下:
server:# 端口port: 8082 spring:application:# 应用名name: ms-users # 数据库datasource:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456url: jdbc:mysql://127.0.0.1:3306/oauth2?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false# Redisredis:port: 6379host: localhosttimeout: 3000database: 1password: 123456# swaggerswagger:base-package: com.zjq.oauth2title: 用户服务API接口文档# Oauth2 客户端信息oauth2:client:client-id: appIdsecret: 123456grant_type: passwordscope: api# 配置 Eureka Server 注册中心eureka:instance:prefer-ip-address: trueinstance-id: ${spring.cloud.client.ip-address}:${server.port}client:service-url:defaultZone: http://localhost:8080/eureka/# oauth2 服务地址service:name:ms-oauth-server: http://ms-oauth2-server/# Mybatismybatis:configuration:map-underscore-to-camel-case: true # 开启驼峰映射# 指标监控健康检查management:endpoints:web:exposure:include: "*" # 暴露的端点logging:pattern:console: '%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
配置类构建
oauth2客户端配置类构建:
/*** oauth2 客户端配置类* @author zjq*/@Component@ConfigurationProperties(prefix = "oauth2.client")@Getter@Setterpublic class OAuth2ClientConfiguration {private String clientId;private String secret;private String grant_type;private String scope;}
编写redisTemplate相关配置类,调整默认的序列化方式。
编写远程请求配置类:
/*** Rest 配置类*/@Configurationpublic class RestTemplateConfiguration {// 负载均衡请求@LoadBalanced@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}}
相关实体类
申请授权返回实体:
/*** 申请授权返回实体* @author zjq*/@Getter@Setterpublic class OAuthUserInfo implements Serializable {private String nickname;private String avatarUrl;private String accessToken;private String expireIn;private List<String> scopes;private String refreshToken;}
登录成功返回实体:
/*** 登录成功返回实体* @author zjq*/@Setter@Getterpublic class LoginUserInfo implements Serializable {private String nickname;private String token;private String avatarUrl;}
登录
登录功能相关代码如下:
/*** 登录** @param account* @param password* @return*/@GetMapping("signin")public ResultInfo signIn(String account, String password) {return userService.signIn(account, password, request.getServletPath());}
/*** 登录** @param account 帐号:用户名或手机或邮箱* @param password 密码* @param path请求路径* @return*/public ResultInfo signIn(String account, String password, String path) {// 参数校验AssertUtil.isNotEmpty(account, "请输入登录帐号");AssertUtil.isNotEmpty(password, "请输入登录密码");// 构建请求头HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);// 构建请求体(请求参数)MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();body.add("username", account);body.add("password", password);body.setAll(BeanUtil.beanToMap(clientOAuth2DataConfiguration));HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);// 设置 AuthorizationrestTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(clientOAuth2DataConfiguration.getClientId(),clientOAuth2DataConfiguration.getSecret()));// 发送请求ResponseEntity<ResultInfo> result = restTemplate.postForEntity(oauthServerName + "oauth/token", entity, ResultInfo.class);// 处理返回结果AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "登录失败");ResultInfo resultInfo = result.getBody();if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {// 登录失败resultInfo.setData(resultInfo.getMessage());return resultInfo;}// 这里的 Data 是一个 LinkedHashMap 转成了域对象 OAuthDinerInfoOAuthUserInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),new OAuthUserInfo(), false);// 根据业务需求返回视图对象LoginUserInfo loginDinerInfo = new LoginUserInfo();loginDinerInfo.setToken(dinerInfo.getAccessToken());loginDinerInfo.setAvatarUrl(dinerInfo.getAvatarUrl());loginDinerInfo.setNickname(dinerInfo.getNickname());return ResultInfoUtil.buildSuccess(path, loginDinerInfo);}
登录验证:
分别启动注册中心,授权认证中心,用户服务,访问http://localhost:8080/,可以看到授权认证中心和用户微服务都已经注册到eureka:
通过接口请求访问http://localhost:8083/user/signin?account=zjq&password=123456,返回如下:
退出登录
退出登录代码如下:
/*** 安全退出** @param access_token* @param authorization* @return*/@GetMapping("user/logout")public ResultInfo logout(String access_token, String authorization) {// 判断 access_token 是否为空,为空将 authorization 赋值给 access_tokenif (StringUtils.isBlank(access_token)) {access_token = authorization;}// 判断 authorization 是否为空if (StringUtils.isBlank(access_token)) {return ResultInfoUtil.buildSuccess(request.getServletPath(), "退出成功");}// 判断 bearer token 是否为空if (access_token.toLowerCase().contains("bearer ".toLowerCase())) {access_token = access_token.toLowerCase().replace("bearer ", "");}// 清除 redis token 信息OAuth2AccessToken oAuth2AccessToken = redisTokenStore.readAccessToken(access_token);if (oAuth2AccessToken != null) {redisTokenStore.removeAccessToken(oAuth2AccessToken);OAuth2RefreshToken refreshToken = oAuth2AccessToken.getRefreshToken();redisTokenStore.removeRefreshToken(refreshToken);}return ResultInfoUtil.buildSuccess(request.getServletPath(), "退出成功");}
至此,我们已经完成了用户的登录和退出流程。
本文内容到此结束了,
如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。
如有错误❌疑问💬欢迎各位指出。
主页:共饮一杯无的博客汇总👨💻
保持热爱,奔赴下一场山海。🏃🏃🏃