A、技术栈
B、本节实现目标
OAuth(开发授权)是一个开放标准,允许用户授权第三方应用,访问他们存储在另外的服务提供者上的信息。而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版,但不兼容OAuth1.0(OAuth1.0已被完全废止)。
Spring Security OAuth2是对OAuth2.0协议的一种实现,并且和Spring Sercurity相辅相成,属于Spring Cloud的体系,与Spring Boot的集成相当便利。在OAuth2.0的协议里包括两个服务提供方,授权服务(也叫认证服务)、资源服务。使用Spring Security OAuth2的时候可以把这两个服务放到同一个应用里面(生产环境不会这样干),也可以建立一个授权服务,对多个资源服务进行授权。
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
即可。
org.springframework.cloud
spring-cloud-starter-oauth2
2.2.4.RELEASE
认证服务(mall-auth)负责认证授权,网关服务(mall-gateway)负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
具体服务:
新建mall-oauth2-module模块,该模块被mall-auth和后面的mall-gateway所依赖。
4.0.0
mall-pom
com.ac
1.0-SNAPSHOT
com.ac
mall-oauth2-module
${mall.version}
mall-oauth2-module
oauth2模块
org.springframework.security.oauth
spring-security-oauth2
2.3.4.RELEASE
org.apache.maven.plugins
maven-compiler-plugin
8
mall-oauth2-module
具体实现代码可参看源码。
新建mall-auth服务,接入OAuth2,颁发token,将token存在redis。
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
4.0.0
mall-pom
com.ac
1.0-SNAPSHOT
com.ac
mall-auth
${mall.version}
mall-auth
授权服务
com.ac
mall-core
1.0-SNAPSHOT
com.ac
mall-common
1.0-SNAPSHOT
com.ac
mall-oauth2
1.0-SNAPSHOT
org.springframework.cloud
spring-cloud-starter-oauth2
com.nimbusds
nimbus-jose-jwt
9.31
org.apache.maven.plugins
maven-compiler-plugin
8
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 params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_PWD.getCode());
params.put("mobile", vo.getMobile());
params.put("password", vo.getPassword());
List 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 params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_SMS.getCode());
params.put("globalCode", vo.getGlobalCode());
params.put("mobile", vo.getMobile());
params.put("code", vo.getMsgCode());
List 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 params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_ONE_KEY.getCode());
params.put("token", vo.getToken());
List 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 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 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 params = getMemberBaseParam(vo, SecurityLoginTypeEnum.APP_ANONYMOUS.getCode());
List grantedAuthorities = new ArrayList<>();
Oauth2TokenDTO oauth2Token = authTokenComponent.getAccessToken(vo.getClientId(), "app", grantedAuthorities, params);
return oauth2Token;
}
private Map getMemberBaseParam(MemberLoginBaseVO vo, String grantType) {
Map 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项目结构
账号密码