创建父项目 food-social-contact-parent,导入公共依赖
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.imooc</groupId>
<artifactId>food-social-contact-parent</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<modules>
<module>ms-registry</module>
<module>ms-gateway</module>
<module>ms-diners</module>
<module>ms-oauth2-server</module>
<module>commons</module>
</modules>
<!-- 可以集中定义依赖资源的版本信息 -->
<properties>
<spring-boot-version>2.3.5.RELEASE</spring-boot-version>
<spring-cloud-version>Hoxton.SR8</spring-cloud-version>
<lombok-version>1.18.16</lombok-version>
<commons-lang-version>3.11</commons-lang-version>
<mybatis-starter-version>2.1.3</mybatis-starter-version>
<mysql-version>8.0.22</mysql-version>
<swagger-starter-version>2.1.5-RELEASE</swagger-starter-version>
<hutool-version>5.4.7</hutool-version>
<guava-version>20.0</guava-version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- 集中定义依赖,不引入 -->
<dependencyManagement>
<dependencies>
<!-- spring boot 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- lombok 依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok-version}</version>
</dependency>
<!-- common-lang3 依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang-version}</version>
</dependency>
<!-- mybatis 依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis-starter-version}</version>
</dependency>
<!-- swagger 依赖 -->
<dependency>
<groupId>com.battcn</groupId>
<artifactId>swagger-spring-boot-starter</artifactId>
<version>${swagger-starter-version}</version>
</dependency>
<!-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-version}</version>
</dependency>
<!-- hutool 依赖 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool-version}</version>
</dependency>
<!-- guava 依赖 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava-version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 集中定义项目所需插件 -->
<build>
<pluginManagement>
<plugins>
<!-- spring boot maven 项目打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
<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>food-social-contact-parentartifactId>
<groupId>com.imoocgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>commonsartifactId>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
dependency>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
dependency>
<dependency>
<groupId>com.battcngroupId>
<artifactId>swagger-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-coreartifactId>
dependency>
dependencies>
project>
/**
* 全局常量类
*/
public class ApiConstant {
// 成功
public static final int SUCCESS_CODE = 1;
// 成功提示信息
public static final String SUCCESS_MESSAGE = "Successful.";
// 错误
public static final int ERROR_CODE = 0;
// 未登录
public static final int NO_LOGIN_CODE = -100;
// 请登录提示信息
public static final String NO_LOGIN_MESSAGE = "Please login.";
// 错误提示信息
public static final String ERROR_MESSAGE = "Oops! Something was wrong.";
}
/**
* 全局异常类
*/
@Getter
@Setter
public class ParameterException extends RuntimeException {
private Integer errorCode;
public ParameterException() {
super(ApiConstant.ERROR_MESSAGE);
this.errorCode = ApiConstant.ERROR_CODE;
}
public ParameterException(Integer errorCode) {
this.errorCode = errorCode;
}
public ParameterException(String message) {
super(message);
this.errorCode = ApiConstant.ERROR_CODE;
}
public ParameterException(Integer errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
}
/**
* 实体对象公共属性
*/
@Getter
@Setter
public class BaseModel implements Serializable {
private Integer id;
private Date createDate;
private Date updateDate;
private int isValid;
}
/**
* 公共返回对象
*/
@Getter
@Setter
@ApiModel(value = "返回说明")
public class ResultInfo<T> implements Serializable {
@ApiModelProperty(value = "成功标识0=失败,1=成功")
private Integer code;
@ApiModelProperty(value = "描述信息")
private String message;
@ApiModelProperty(value = "访问路径")
private String path;
@ApiModelProperty(value = "返回数据对象")
private T data;
}
/**
* 食客实体类
*/
@Getter
@Setter
public class Diners extends BaseModel {
// 主键
private Integer id;
// 用户名
private String username;
// 昵称
private String nickname;
// 密码
private String password;
// 手机号
private String phone;
// 邮箱
private String email;
// 头像
private String avatarUrl;
// 角色
private String roles;
}
/**
* 公共返回对象工具类
*/
public class ResultInfoUtil {
/**
* 请求出错返回
*
* @param path 请求路径
* @param
* @return
*/
public static <T> ResultInfo<T> buildError(String path) {
ResultInfo<T> resultInfo = build(ApiConstant.ERROR_CODE,
ApiConstant.ERROR_MESSAGE, path, null);
return resultInfo;
}
/**
* 请求出错返回
*
* @param errorCode 错误代码
* @param message 错误提示信息
* @param path 请求路径
* @param
* @return
*/
public static <T> ResultInfo<T> buildError(int errorCode, String message, String path) {
ResultInfo<T> resultInfo = build(errorCode, message, path, null);
return resultInfo;
}
/**
* 请求成功返回
*
* @param path 请求路径
* @param
* @return
*/
public static <T> ResultInfo<T> buildSuccess(String path) {
ResultInfo<T> resultInfo = build(ApiConstant.SUCCESS_CODE,
ApiConstant.SUCCESS_MESSAGE, path, null);
return resultInfo;
}
/**
* 请求成功返回
*
* @param path 请求路径
* @param data 返回数据对象
* @param
* @return
*/
public static <T> ResultInfo<T> buildSuccess(String path, T data) {
ResultInfo<T> resultInfo = build(ApiConstant.SUCCESS_CODE,
ApiConstant.SUCCESS_MESSAGE, path, data);
return resultInfo;
}
/**
* 构建返回对象方法
*
* @param code
* @param message
* @param path
* @param data
* @param
* @return
*/
public static <T> ResultInfo<T> build(Integer code, String message, String path, T data) {
if (code == null) {
code = ApiConstant.SUCCESS_CODE;
}
if (message == null) {
message = ApiConstant.SUCCESS_MESSAGE;
}
ResultInfo resultInfo = new ResultInfo();
resultInfo.setCode(code);
resultInfo.setMessage(message);
resultInfo.setPath(path);
resultInfo.setData(data);
return resultInfo;
}
}
<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>food-social-contact-parentartifactId>
<groupId>com.imoocgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>ms-registryartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
project>
server:
port: 8080
spring:
application:
name: ms-registry
# 配置 Eureka Server 注册中心
eureka:
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://localhost:8080/eureka/
<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>food-social-contact-parentartifactId>
<groupId>com.imoocgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>ms-oauth2-serverartifactId>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>com.imoocgroupId>
<artifactId>commonsartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
project>
server:
port: 8082 # 端口
spring:
application:
name: ms-oauth2-server # 应用名
# 数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/orgnization?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
redis:
port: 6379
host: 192.168.38.22
timeout: 3000
database: 1
# swagger
swagger:
base-package: com.hh.oauth2
title: 美食社交食客API接口文档
# Oauth2
client:
oauth2:
client-id: appId # 客户端标识 ID
secret: 123456 # 客户端安全码
# 授权类型
grant_types:
- password
- refresh_token
# token 有效时间,单位秒
token-validity-time: 3600
refresh-token-validity-time: 3600
# 客户端访问范围
scopes:
- api
- all
# 配置 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/
# 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'
自定义认证数据源从数据库中获取用户信息:
@Service
public class UserService implements UserDetailsService {
@Resource
private DinersMapper dinersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AssertUtil.isNotEmpty(username, "请输入用户名");
Diners diners = dinersMapper.selectByAccountInfo(username);
if (diners == null) {
throw new UsernameNotFoundException("用户名或密码错误,请重新输入");
}
return new User(
username,
diners.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(diners.getRoles())
);
}
}
@Mapper
public interface DinersMapper {
/**
* 根据用户名 or 手机号 or 邮箱查询用户信息
*/
@Select("select id, username, nickname, phone, email, " +
"password, avatar_url, roles, is_valid from t_diners where " +
"(username = #{account} or phone = #{account} or email = #{account})")
Diners selectByAccountInfo(@Param("account") String account);
}
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 注入 Redis 连接工厂
@Resource
private RedisConnectionFactory redisConnectionFactory;
/**
* 初始化 RedisTokenStore 用于将 token 存储至 Redis
*/
@Bean
public RedisTokenStore redisTokenStore() {
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
// 设置key的层级前缀,方便查询
redisTokenStore.setPrefix("TOKEN:");
return redisTokenStore;
}
/**
* 初始化密码编码器,用 MD5 加密密码
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
/**
* 密码加密
* @param rawPassword 原始密码
* @return 加密后的密码
*/
@Override
public String encode(CharSequence rawPassword) {
return DigestUtil.md5Hex(rawPassword.toString());
}
/**
* 校验密码
* @param rawPassword 原始密码
* @param encodedPassword 加密密码
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return DigestUtil.md5Hex(rawPassword.toString()).equals(encodedPassword);
}
};
}
/**
* 初始化认证管理对象
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 放行和认证规则
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 放行的请求
.antMatchers("/oauth/**", "/actuator/**").permitAll()
// 其他请求必须认证才能访问
.anyRequest().authenticated()
.and()
.csrf().disable();
}
}
# Oauth2
client:
oauth2:
client-id: appId # 客户端标识 ID
secret: 123456 # 客户端安全码
# 授权类型
grant_types:
- password
- refresh_token
# token 有效时间,单位秒
token-validity-time: 3600
refresh-token-validity-time: 3600
# 客户端访问范围
scopes:
- api
- all
/**
* 客户端配置类
*/
@Component
@ConfigurationProperties(prefix = "client.oauth2")
@Data
public class ClientOAuth2DataConfiguration {
// 客户端标识 ID
private String clientId;
// 客户端安全码
private String secret;
// 授权类型
private String[] grantTypes;
// token有效期
private int tokenValidityTime;
// refresh-token有效期
private int refreshTokenValidityTime;
// 客户端访问范围
private String[] scopes;
}
/**
* 授权服务器配置类
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// RedisTokenSore
@Resource
private RedisTokenStore redisTokenStore;
// 认证管理对象
@Resource
private AuthenticationManager authenticationManager;
// 密码编码器
@Resource
private PasswordEncoder passwordEncoder;
// 客户端配置类
@Resource
private ClientOAuth2DataConfiguration clientOAuth2DataConfiguration;
// 登录校验
@Resource
private UserService userService;
/**
* 客户端配置 - 授权模型
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 客户端标识 ID
.withClient(clientOAuth2DataConfiguration.getClientId())
// 客户端安全码
.secret(passwordEncoder.encode(clientOAuth2DataConfiguration.getSecret()))
// 授权类型
.authorizedGrantTypes(clientOAuth2DataConfiguration.getGrantTypes())
// token 有效期
.accessTokenValiditySeconds(clientOAuth2DataConfiguration.getTokenValidityTime())
// 刷新 token 的有效期
.refreshTokenValiditySeconds(clientOAuth2DataConfiguration.getRefreshTokenValidityTime())
// 客户端访问范围
.scopes(clientOAuth2DataConfiguration.getScopes());
}
/**
* 配置授权以及令牌的访问端点和令牌服务
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 认证器
endpoints.authenticationManager(authenticationManager)
// 刷新令牌必须配置userDetailsService,用来刷新令牌时的认证
.userDetailsService(userService)
// token 存储的方式:Redis
.tokenStore(redisTokenStore);
}
/**
* 配置令牌端点安全约束
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许访问 token 的公钥,默认 /oauth/token_key 是受保护的
security.tokenKeyAccess("permitAll()")
// 允许检查 token 的状态,默认 /oauth/check_token 是受保护的
.checkTokenAccess("permitAll()");
}
}
查看注册中心是否注册 ms-oauth2-server服务:
SpringSecurity Oauth 的访问令牌接口:/oauth/token
认证参数:
请求体参数:
oauth2控制器,让/oauth/token请求走自己的控制器接口:
/**
* 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 返回对象
*/
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.hh.commons.model.domain;
@Data
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;
}
}
/**
* 授权服务
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// 省略....
/**
* 配置授权以及令牌的访问端点和令牌服务
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 认证器
endpoints.authenticationManager(authenticationManager)
// 刷新令牌必须配置userDetailsService,用来刷新令牌时的认证
.userDetailsService(userService)
// token 存储的方式:Redis
.tokenStore(redisTokenStore)
// 令牌增强对象,增强返回的结果
.tokenEnhancer((accessToken, authentication) -> {
// 获取登录用户的信息,然后设置
SignInIndentity signInIdentity = (SignInIndentity) 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;
});
}
}
启动项目测试:
目前已经在认证授权中心的登录校验已经写完了,网关路由到ms-dinners服务,在ms-dinners服务远程调用授权认证服务完成认证功能。
下面就是完成食客服务ms-dinners的登录业务逻辑,网关路由到ms-dinners服务,在ms-dinners服务远程调用授权认证服务完成认证功能。
<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>food-social-contact-parentartifactId>
<groupId>com.imoocgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>ms-dinersartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.imoocgroupId>
<artifactId>commonsartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
project>
server:
port: 8081 # 端口
spring:
application:
name: ms-diners # 应用名
# 数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/orgnization?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
redis:
port: 6379
host: 192.168.38.22
timeout: 3000
database: 1
# swagger
swagger:
base-package: com.hh.oauth2
title: 美食社交食客API接口文档
# Oauth2 客户端信息
oauth2:
client:
client-id: appId
secret: 123456
grant_type: password
scope: api
# oauth2 服务地址
service:
name:
ms-oauth-server: http://ms-oauth2-server/
# 配置 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/
logging:
pattern:
console: '%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
SpringBoot 配置文件:
# Oauth2 客户端信息
oauth2:
client:
client-id: appId
secret: 123456
grant_type: password
scope: api
/**
* 客户端配置类
*/
@Component
@ConfigurationProperties(prefix = "oauth2.client")
@Getter
@Setter
public class OAuth2ClientConfiguration {
private String clientId;
private String secret;
private String grant_type;
private String scope;
}
RestTemplate是执行HTTP请求的同步阻塞式的客户端,RestTemplate类通过为HTTP方法(例如GET,POST,PUT,DELETE等)提供重载的方法,提供了一种非常方便的方法访问基于HTTP的Web服务。
/**
* Rest 配置类
*/
@Configuration
public class RestTemplateConfiguration {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
/**
* 食客服务控制层
*/
@RestController
@Api(tags = "食客相关接口")
public class DinersController {
@Resource
private DinersService dinersService;
@Resource
private HttpServletRequest request;
/**
* 登录
*/
@GetMapping("signin")
public ResultInfo signIn(String account, String password) {
return dinersService.signIn(account, password, request.getServletPath());
}
}
/**
* 食客服务业务逻辑层
*/
@Service
public class DinersService {
@Resource
private RestTemplate restTemplate;
@Value("${service.name.ms-oauth-server}")
private String oauthServerName;
@Resource
private OAuth2ClientConfiguration oAuth2ClientConfiguration;
/**
* 登录
*
* @param account 帐号:用户名或手机或邮箱
* @param password 密码
* @param path 请求路径
*/
public ResultInfo signIn(String account, String password, String path) {
// 参数校验
AssertUtil.isNotEmpty(account, "请输入登录帐号");
AssertUtil.isNotEmpty(password, "请输入登录密码");
// 模拟使用postForObject模拟表单数据提交,即:提交x-www-form-urlencoded格式的数据
// 构建请求头
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(oAuth2ClientConfiguration));
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// 设置 Authorization
restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(
oAuth2ClientConfiguration.getClientId(),
oAuth2ClientConfiguration.getSecret()));
// 发送请求:请求路径url,请求体,响应数据类型
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
OAuthDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),new OAuthDinerInfo(), false);
// 根据业务需求返回视图对象
LoginDinerInfo loginDinerInfo = new LoginDinerInfo();
loginDinerInfo.setToken(dinerInfo.getAccessToken());
loginDinerInfo.setAvatarUrl(dinerInfo.getAvatarUrl());
loginDinerInfo.setNickname(dinerInfo.getNickname());
return ResultInfoUtil.buildSuccess(path, loginDinerInfo);
}
}
访问/oauth/token接口获取token的返回结果:
package com.hh.diners.domain;
@Getter
@Setter
public class OAuthDinerInfo implements Serializable {
private String nickname;
private String avatarUrl;
private String accessToken;
private String expireIn;
private List<String> scopes;
private String refreshToken;
}
返回给前端的响应类:
package com.hh.diners.vo;
@Setter
@Getter
public class LoginDinerInfo implements Serializable {
private String nickname;
private String token;
private String avatarUrl;
}
其实上面的操作就是使用RestTemplate发起HTTP POST请求访问oauth/token接口路径获取token的过程,即使用postForObject模拟表单数据提交x-www-form-urlencoded格式的数据,其中请求数据为:
启动 ms-registry 注册中心服务、ms-oauth-server 认证授权中心服务、ms-dinners 食客服务
/**
* 资源服务
*/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Resource
private MyAuthenticationEntryPoint authenticationEntryPoint;
@Override
public void configure(HttpSecurity http) throws Exception {
// 配置放行的资源
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.requestMatchers().antMatchers("/user/**");
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationEntryPoint(authenticationEntryPoint);
}
}
认证失败后的自定义处理逻辑:
/**
* 认证失败处理
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Resource
private ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
// 返回 JSON
response.setContentType("application/json;charset=utf-8");
// 状态码 401
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// 写出
PrintWriter out = response.getWriter();
String errorMessage = authException.getMessage();
if (StringUtils.isBlank(errorMessage)) {
errorMessage = "登录失效!";
}
ResultInfo result = ResultInfoUtil.buildError(ApiConstant.ERROR_CODE,
errorMessage, request.getRequestURI());
out.write(objectMapper.writeValueAsString(result));
out.flush();
out.close();
}
}
/**
* 用户中心
*/
@RestController
public class UserController {
@Resource
private HttpServletRequest request;
@Resource
private RedisTokenStore redisTokenStore;
@GetMapping("user/me")
public ResultInfo getCurrentUser(Authentication authentication) {
// 获取登录用户的信息,然后设置
SignInIndentity signInIdentity = (SignInIndentity) authentication.getPrincipal();
// 转为前端可用的视图对象
SignInDinerInfo dinerInfo = new SignInDinerInfo();
BeanUtils.copyProperties(signInIdentity, dinerInfo);
return ResultInfoUtil.buildSuccess(request.getServletPath(), dinerInfo);
}
}
package com.imooc.commons.model.vo;
@Getter
@Setter
@ApiModel(value = "SignInDinerInfo", description = "登录用户信息")
public class SignInDinerInfo implements Serializable {
@ApiModelProperty("主键")
private Integer id;
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("昵称")
private String nickname;
@ApiModelProperty("手机号")
private String phone;
@ApiModelProperty("邮箱")
private String email;
@ApiModelProperty("头像")
private String avatarUrl;
@ApiModelProperty("角色")
private String roles;
}
① 首先获取access_token :
② 请求路径上携带 access_token 访问受限资源
③ Authentication中配置access_token 访问受限资源
/**
* 用户中心
*/
@RestController
public class UserController {
@Resource
private HttpServletRequest request;
@Resource
private RedisTokenStore redisTokenStore;
/**
* 安全退出
*
* @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(), "退出成功");
}
}
所有的请求先到网关,在网关中做身份认证
<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>food-social-contact-parentartifactId>
<groupId>com.imoocgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>ms-gatewayartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.imoocgroupId>
<artifactId>commonsartifactId>
<version>1.0-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>com.battcngroupId>
<artifactId>swagger-spring-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
dependencies>
project>
server:
port: 80
spring:
application:
name: ms-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启配置注册中心进行路由功能
lower-case-service-id: true # 将服务名称转小写
routes:
- id: ms-diners
uri: lb://ms-diners
predicates:
- Path=/diners/**
filters:
- StripPrefix=1
- id: ms-oauth2-server
uri: lb://ms-oauth2-server
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
# 配置 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/
logging:
pattern:
console: '%d{HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
/**
* 网关白名单配置
*/
@Data
@Component
@ConfigurationProperties(prefix = "secure.ignore")
public class IgnoreUrlsConfig {
private List<String> urls;
}
@Configuration
public class RestTemplateConfiguration {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
/**
* 网关全局过滤器
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Resource
private IgnoreUrlsConfig ignoreUrlsConfig;
@Resource
private RestTemplate restTemplate;
@Resource
private HandleException handleException;
/**
* 身份校验处理
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 判断当前的请求是否在白名单中
AntPathMatcher pathMatcher = new AntPathMatcher();
boolean flag = false;
String path = exchange.getRequest().getURI().getPath();
for (String url : ignoreUrlsConfig.getUrls()) {
if (pathMatcher.match(url, path)) {
flag = true;
break;
}
}
// 白名单放行
if (flag) {
return chain.filter(exchange);
}
// 获取 access_token
String access_token = exchange.getRequest().getQueryParams().getFirst("access_token");
// 判断 access_token 是否为空
if (StringUtils.isBlank(access_token)) {
return handleException.writeError(exchange, "请登录");
}
// 校验 token 是否有效
String checkTokenUrl = "http://ms-oauth2-server/oauth/check_token?token=".concat(access_token);
try {
// 发送远程请求,验证 token
ResponseEntity<String> entity = restTemplate.getForEntity(checkTokenUrl, String.class);
// token 无效的业务逻辑处理
if (entity.getStatusCode() != HttpStatus.OK) {
return handleException.writeError(exchange,
"Token was not recognised, token: ".concat(access_token));
}
if (StringUtils.isBlank(entity.getBody())) {
return handleException.writeError(exchange,
"This token is invalid: ".concat(access_token));
}
} catch (Exception e) {
return handleException.writeError(exchange,
"Token was not recognised, token: ".concat(access_token));
}
// 放行
return chain.filter(exchange);
}
/**
* 网关过滤器的排序,数字越小优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}
@Component
public class HandleException {
@Resource
private ObjectMapper objectMapper;
public Mono<Void> writeError(ServerWebExchange exchange, String error) {
ServerHttpResponse response = exchange.getResponse();
ServerHttpRequest request = exchange.getRequest();
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
// 用户没有登录,请登录
ResultInfo resultInfo = ResultInfoUtil.buildError(ApiConstant.NO_LOGIN_CODE, ApiConstant.NO_LOGIN_MESSAGE, request.getURI().getPath());
String resultInfoJson = null;
DataBuffer buffer = null;
try {
resultInfoJson = objectMapper.writeValueAsString(resultInfo);
buffer = response.bufferFactory().wrap(resultInfoJson.getBytes(Charset.forName("UTF-8")));
} catch (JsonProcessingException ex) {
ex.printStackTrace();
}
return response.writeWith(Mono.just(buffer));
}
}
所有的请求都经过网关,网关根据请求路径路由到指定的服务。网关路由到ms-dinners服务完成认证登录获取令牌token。
网关路由到 ms-oauth2-server 服务获取用户登录信息: