总概
A、技术栈
- 开发语言:Java 1.8
- 数据库:MySQL、Redis、MongoDB、Elasticsearch
- 微服务框架:Spring Cloud Alibaba
- 微服务网关:Spring Cloud Gateway
- 服务注册和配置中心:Nacos
- 分布式事务:Seata
- 链路追踪框架:Sleuth
- 服务降级与熔断:Sentinel
- ORM框架:MyBatis-Plus
- 分布式任务调度平台:XXL-JOB
- 消息中间件:RocketMQ
- 分布式锁:Redisson
- 权限:OAuth2
- DevOps:Jenkins、Docker、K8S
B、本节实现目标
- 新建mall-auth服务,完成授权功能
一、OAuth2.0介绍
OAuth(开发授权)是一个开放标准,允许用户授权第三方应用,访问他们存储在另外的服务提供者上的信息。而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版,但不兼容OAuth1.0(OAuth1.0已被完全废止)。
二、spring-cloud-starter-oauth2使用
2.1 Spring-Security-OAuth2介绍
Spring Security OAuth2是对OAuth2.0协议的一种实现,并且和Spring Sercurity相辅相成,属于Spring Cloud的体系,与Spring Boot的集成相当便利。在OAuth2.0的协议里包括两个服务提供方,授权服务(也叫认证服务)、资源服务。使用Spring Security OAuth2的时候可以把这两个服务放到同一个应用里面(生产环境不会这样干),也可以建立一个授权服务,对多个资源服务进行授权。
2.2 pom.xml依赖
SpringCloud微服务全家桶中有spring-cloud-starter-security
依赖组件,并且spring-cloud-starter-oauth2
依赖了spring-cloud-starter-security
。spring-cloud-starter-security
依赖了spring-boot-starter-security
,因此添加spring-cloud-starter-oauth2
即可。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
三、架构说明
认证服务(mall-auth)负责认证授权,网关服务(mall-gateway)负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
具体服务:
- [mall-auth]:认证服务,负责对登录用户进行认证授权颁发token。
- [mall-gateway]:网关服务,负责请求转发和校验认证和鉴权。
- [mall-member]:受保护的API服务,用户鉴权通过后可以访问该服务,该类服务还有[mall-product]、[mall-search]等等。
四、代码实现
4.1 mall-oauth2-module模块
新建mall-oauth2-module模块,该模块被mall-auth和后面的mall-gateway所依赖。
4.1.1 pom.xml
<?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>
<parent>
<artifactId>mall-pom</artifactId>
<groupId>com.ac</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ac</groupId>
<artifactId>mall-oauth2-module</artifactId>
<version>${mall.version}</version>
<name>mall-oauth2-module</name>
<description>oauth2模块</description>
<dependencies>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.1.2 项目结构
mall-oauth2-module
具体实现代码可参看源码。
4.2 mall-auth服务
新建mall-auth服务,接入OAuth2,颁发token,将token存在redis。
4.2.1 数据库脚本
CREATE TABLE `oauth_client_details` (
`client_id` VARCHAR(128)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`resource_ids` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`client_secret` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`scope` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`authorized_grant_types` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`web_server_redirect_uri` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`authorities` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`access_token_validity` INT DEFAULT NULL,
`refresh_token_validity` INT DEFAULT NULL,
`additional_information` VARCHAR(4096)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`autoapprove` VARCHAR(256)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`client_secret_str` VARCHAR(20)CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`client_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
INSERT INTO `oauth_client_details` VALUES ('app',NULL,'$2a$10$i3F515wEDiB4Gvj9ym9Prui0dasRttEUQ9ink4Wpgb4zEDCAlV8zO','all','APP_SMS,APP_ONE_KEY,APP_SOCIAL,APP_PWD,ADMIN_PWD,APP_SOCIAL,APP_ANONYMOUS,APP_QRCODE,authorization_code,password,refresh_token','http://a.qimiao.com',NULL,2592000,2592000,NULL,NULL,'app'),('h5',NULL,'$2a$10$i3F515wEDiB4Gvj9ym9Prui0dasRttEUQ9ink4Wpgb4zEDCAlV8zO','all','APP_SMS,APP_ONE_KEY,APP_SOCIAL,APP_PWD,ADMIN_PWD,APP_SOCIAL,APP_ANONYMOUS,APP_QRCODE,authorization_code,password,refresh_token',NULL,NULL,2592000,2592000,NULL,NULL,'app'),('mini',NULL,'$2a$10$i3F515wEDiB4Gvj9ym9Prui0dasRttEUQ9ink4Wpgb4zEDCAlV8zO','all','APP_SMS,APP_ONE_KEY,APP_SOCIAL,APP_PWD,ADMIN_PWD,APP_SOCIAL,APP_ANONYMOUS,APP_QRCODE,authorization_code,password,refresh_token',NULL,NULL,2592000,2592000,NULL,NULL,'app'),('web',NULL,'$2a$10$i3F515wEDiB4Gvj9ym9Prui0dasRttEUQ9ink4Wpgb4zEDCAlV8zO','all','APP_SMS,APP_ONE_KEY,APP_SOCIAL,APP_PWD,ADMIN_PWD,APP_SOCIAL,APP_ANONYMOUS,APP_QRCODE,authorization_code,password,refresh_token',NULL,NULL,2592000,2592000,NULL,NULL,'app');
4.2.2 pom.xml
<?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>
<parent>
<artifactId>mall-pom</artifactId>
<groupId>com.ac</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.ac</groupId>
<artifactId>mall-auth</artifactId>
<version>${mall.version}</version>
<name>mall-auth</name>
<description>授权服务</description>
<dependencies>
<dependency>
<groupId>com.ac</groupId>
<artifactId>mall-core</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.ac</groupId>
<artifactId>mall-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.ac</groupId>
<artifactId>mall-oauth2</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.31</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2.3 AuthController
package com.ac.auth.controller;
import com.ac.auth.component.AuthRedisHelper;
import com.ac.auth.component.AuthTokenComponent;
import com.ac.auth.dto.Oauth2TokenDTO;
import com.ac.oauth2.enums.SecurityLoginTypeEnum;
import com.ac.auth.util.IpUtil;
import com.ac.auth.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jodd.util.StringUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j
@Api(tags = "会员授权")
@RestController
@RequestMapping("oauth")
public class AuthController {
@Resource
private AuthTokenComponent authTokenComponent;
@Resource
private AuthRedisHelper authRedisHelper;
@Resource
private RedissonClient redissonClient;
@SneakyThrows
@PostMapping("pwdLogin")
@ApiOperation(value = "密码登录")
public Oauth2TokenDTO pwdLogin(@RequestBody MemberLoginPwdVO vo, HttpServletRequest request) {
if (StringUtil.isEmpty(vo.getClientId())) {
vo.setClientId("app");
}
vo.setIp(IpUtil.ip(request));
Map<String, String> params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_PWD.getCode());
params.put("mobile", vo.getMobile());
params.put("password", vo.getPassword());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Oauth2TokenDTO oauth2Token = authTokenComponent.getAccessToken(vo.getClientId(), "app", grantedAuthorities, params);
return oauth2Token;
}
@SneakyThrows
@PostMapping("msgCodeLogin")
@ApiOperation(value = "短信验证码登录")
public Oauth2TokenDTO msgCodeLogin(@RequestBody MemberLoginMsgCodeVO vo, HttpServletRequest request) {
log.info("msgCodeLogin:mobile={}", vo.getMobile());
RLock rdsLock = redissonClient.getLock(vo.getMobile());
try {
rdsLock.lock(5, TimeUnit.SECONDS);
Boolean delRecord = authRedisHelper.getDelRecord(vo.getMobile());
if (delRecord) {
throw new RuntimeException("用户已注销");
}
vo.setIp(IpUtil.ip(request));
Map<String, String> params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_SMS.getCode());
params.put("globalCode", vo.getGlobalCode());
params.put("mobile", vo.getMobile());
params.put("code", vo.getMsgCode());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Oauth2TokenDTO oauth2Token = authTokenComponent.getAccessToken(vo.getClientId(), "app", grantedAuthorities, params);
return oauth2Token;
} finally {
// 释放锁
if (rdsLock.isLocked()) {
rdsLock.unlock();
}
}
}
@SneakyThrows
@PostMapping("oneKeyLogin")
@ApiOperation(value = "一键登录")
public Oauth2TokenDTO oneKeyLogin(@RequestBody MemberLoginOneKeyVO vo, HttpServletRequest request) {
vo.setIp(IpUtil.ip(request));
Map<String, String> params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_ONE_KEY.getCode());
params.put("token", vo.getToken());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Oauth2TokenDTO oauth2Token = authTokenComponent.getAccessToken(vo.getClientId(), "app", grantedAuthorities, params);
return oauth2Token;
}
@SneakyThrows
@PostMapping("socialLogin")
@ApiOperation(value = "第三方登录")
public Oauth2TokenDTO socialLogin(@RequestBody MemberLoginSocialVO vo, HttpServletRequest request) {
RLock rdsLock = redissonClient.getLock(vo.getAcc());
try {
rdsLock.lock(5, TimeUnit.SECONDS);
if (StringUtil.isEmpty(vo.getClientId())) {
vo.setClientId("app");
}
vo.setIp(IpUtil.ip(request));
Map<String, String> params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_SOCIAL.getCode());
params.put("platform", vo.getPlatform());
params.put("socialType", vo.getSocialType().getCode());
params.put("acc", vo.getAcc());
params.put("uid", vo.getUid());
params.put("iconUrl", vo.getIconUrl());
params.put("nickName", vo.getNickName());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Oauth2TokenDTO oauth2Token = authTokenComponent.getAccessToken(vo.getClientId(), "app", grantedAuthorities, params);
return oauth2Token;
} finally {
// 释放锁
if (rdsLock.isLocked()) {
rdsLock.unlock();
}
}
}
@SneakyThrows
@PostMapping("visitor")
@ApiOperation(value = "游客登录")
public Oauth2TokenDTO visitorLogin(@RequestBody MemberLoginVisitorVO vo, HttpServletRequest request) {
vo.setIp(IpUtil.ip(request));
Map<String, String> params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_ANONYMOUS.getCode());
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
Oauth2TokenDTO oauth2Token = authTokenComponent.getAccessToken(vo.getClientId(), "app", grantedAuthorities, params);
return oauth2Token;
}
private Map<String, String> getMemberBaseParam(MemberLoginBaseVO vo, String grantType) {
Map<String, String> params = new HashMap<>();
params.put("client_id", vo.getClientId());
params.put("client_secret", "app");
params.put("grant_type", grantType);
params.put("scope", "all");
params.put("platform", vo.getPlatform());
//附加信息
params.put("version", vo.getVersion());
params.put("device", vo.getDevice());
params.put("iemi", vo.getIemi());
params.put("location", vo.getLocation());
params.put("ip", vo.getIp());
params.put("recommendCode", vo.getRecommendCode());
return params;
}
}
mall-auth项目结构
五、测试
5.1 账号密码
账号密码
5.2 短信验证码
短信验证码