- [认证授权中心自定义令牌增强](https://cloud.tencent.com/developer)
- [自定义认证端点返回结果](https://cloud.tencent.com/developer)
- [登录逻辑调整,增强令牌返回参数](https://cloud.tencent.com/developer)
- [测试验证](https://cloud.tencent.com/developer)
- [用户微服务构建](https://cloud.tencent.com/developer)
- [配置类构建](https://cloud.tencent.com/developer)
- [相关实体类](https://cloud.tencent.com/developer)
- [登录](https://cloud.tencent.com/developer)
- [退出登录](https://cloud.tencent.com/developer)
在之前的博客我写了 SpringCloud整合spring security+ oauth2+Redis实现认证授权,本文对返回的token实现自定义增强令牌返回结果,以及对于oauth2存在Redis的数据进行解释。
访问oauth/token,oauth2默认返回的授权token信息如下:
如果不自定义可以看到访问oauth/token,默认访问的是TokenEndpoint下的接口
在授权服务中自定义oauth2控制器实现自定义令牌参数返回,代码如下:
package com.zjq.oauth2.server.controller;
import com.zjq.commons.model.domain.ResultInfo;
import com.zjq.commons.utils.ResultInfoUtil;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.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 {
@Resource
private TokenEndpoint tokenEndpoint;
@Resource
private 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 com.zjq.commons.model.domain;
import cn.hutool.core.util.StrUtil;
import com.google.common.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 2022/10/12
*/
@Getter
@Setter
public 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;
// 获取角色信息
@Override
public 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_USER
this.authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList("ROLE_USER");
}
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.isValid == 0 ? false : true;
}
}
登录后返回登录认证对象:
@Resource
private UsersMapper usersMapper;
@Override
public 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
*/
@Override
public 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="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/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.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/oauth2?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
redis:
port: 6379
host: localhost
timeout: 3000
database: 1
password: 123456
# swagger
swagger:
base-package: com.zjq.oauth2
title: 用户服务API接口文档
# Oauth2 客户端信息
oauth2:
client:
client-id: appId
secret: 123456
grant_type: password
scope: api
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true
instance-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/
# Mybatis
mybatis:
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
@Setter
public class OAuth2ClientConfiguration {
private String clientId;
private String secret;
private String grant_type;
private String scope;
}
编写redisTemplate相关配置类,调整默认的序列化方式。
编写远程请求配置类:
/**
* Rest 配置类
*/
@Configuration
public class RestTemplateConfiguration {
// 负载均衡请求
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
申请授权返回实体:
/**
* 申请授权返回实体
* @author zjq
*/
@Getter
@Setter
public 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
@Getter
public 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);
// 设置 Authorization
restTemplate.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 转成了域对象 OAuthDinerInfo
OAuthUserInfo 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_token
if (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(), "退出成功");
}
至此,我们已经完成了用户的登录和退出流程。
本文内容到此结束了, 如有收获欢迎点赞👍收藏💖关注✔️,您的鼓励是我最大的动力。 如有错误❌疑问💬欢迎各位指出。 主页:共饮一杯无的博客汇总👨💻 保持热爱,奔赴下一场山海。🏃🏃🏃