公共项目
全局常量类
package com.itkaka.commons.constant;
/**
* 全局常量类
*/
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 String ERROR_MESSAGE = "Oops! Something was wrong!";
// 未登录
public static final int NO_LOGIN_CODE = -100;
// 请登录提示信息
public static final String NO_LOGIN_MESSAGE = "Please login!";
// Feed 默认每页条数
// 在 fs_feed 服务时候再新加
public static final int PAGE_SIZE = 20;
}
package com.itkaka.commons.constant;
/**
* 全局常量类
*/
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 String ERROR_MESSAGE = "Oops! Something was wrong!";
// 未登录
public static final int NO_LOGIN_CODE = -100;
// 请登录提示信息
public static final String NO_LOGIN_MESSAGE = "Please login!";
}
全局异常类
package com.itkaka.commons.exception;
import com.itkaka.commons.constant.ApiConstant;
import lombok.Getter;
import lombok.Setter;
/**
* 全局异常类
*/
@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;
}
}
断言工具类
package com.itkaka.commons.utils;
import cn.hutool.core.util.StrUtil;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.exception.ParameterException;
/**
* 断言工具类
*/
public class AssertUtil {
/**
* 判断字符串非空
*
* @param str
* @param message
*/
public static void isNotEmpty(String str, String... message) {
if (StrUtil.isBlank(str)) {
execute(message);
}
}
/**
* 判断对象非空
*
* @param obj
* @param message
*/
public static void isNotNull(Object obj, String... message) {
if (obj == null) {
execute(message);
}
}
/**
* 判断结果是否为真
*
* @param isTrue
* @param message
*/
public static void isTrue(boolean isTrue, String... message) {
if (isTrue) {
execute(message);
}
}
/**
* 最终执行方法
*
* @param message
*/
private static void execute(String... message) {
String msg = ApiConstant.ERROR_MESSAGE;
if (message != null && message.length > 0) {
msg = message[0];
}
throw new ParameterException(msg);
}
}
公共返回对象
package com.itkaka.commons.model.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
/**
* 公共返回对象
*/
@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;
}
公共返回对象工具类
package com.itkaka.commons.utils;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.model.domain.ResultInfo;
/**
* 公共返回对象工具类
*/
public class ResultInfoUtil {
/**
* 请求出错返回
*
* @param path 请求路径
* @param <T>
* @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 <T>
* @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 <T>
* @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 <T>
* @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 <T>
* @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;
}
}
认证授权中心
创建项目
创建子模块 fs_oauth
<?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>food_social</artifactId>
<groupId>com.itkaka</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fs_oauth</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<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>
<!-- spring cloud security -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<!-- spring cloud oauth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<!-- commons 公共项目 -->
<dependency>
<groupId>com.itkaka</groupId>
<artifactId>fs_commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 自定义的元数据依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 单元测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
配置文件
server:
port: 8092 # 端口
spring:
application:
name: fs_oauth # 应用名
# 数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/db_lezijie_food_social?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
# redis:
# port: 6379
# host: 192.168.10.101
# timeout: 3000
# database: 1
# password: 123456
# swagger
swagger:
base-package: com.itkaka.oauth2
title: 美食社交API接口文档
# Oauth2
client:
oauth2:
client-id: appId # 客户端标识 ID
secret: 123456 # 客户端安全码
# 授权类型
grant_types:
- password
- refresh_token
# token 有效时间,单位秒
token-validity-time: 2592000
refresh-token-validity-time: 2592000
# 客户端访问范围
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:8090/eureka/
# Mybatis
mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰映射
# 指标监控健康检查
management:
endpoints:
web:
exposure:
include: "*" # 暴露的端点
# 配置日志
logging:
pattern:
console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'
Security配置类
package com.itkaka.oauth2.config;
import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.annotation.Resource;
/**
* Spring Security 配置类
*/
@EnableWebSecurity
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// Redis 连接工厂
@Resource
private RedisConnectionFactory connectionFactory;
// 初始化 RedisTokenStore 用于将 Token 存储至 Redis
@Bean
public RedisTokenStore redisTokenStore() {
RedisTokenStore redisTokenStore = new RedisTokenStore(connectionFactory);
redisTokenStore.setPrefix("TOKEN:"); // 设置好 key 的层级,方便查询
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 加密后
* @return
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(DigestUtil.md5Hex(rawPassword.toString()));
}
};
}
// 初始化认证管理
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 放行和认证规则
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// csrf 防御机制(跨域请求伪造) 禁用,方便测试开发
http.csrf().disable()
// 请求认证规则
.authorizeRequests()
// 放行请求
.antMatchers("/oauth/**", "/actuator/**").permitAll()
.and()
.authorizeRequests()
// 其他请求必须认证才能访问
.anyRequest().authenticated();
}
}
授权服务
package com.itkaka.oauth2.config;
import com.itkaka.commons.model.domain.SignInIdentity;
import com.itkaka.oauth2.service.UserService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 授权服务
*/
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// 密码编码器
@Resource
private PasswordEncoder passwordEncoder;
// 客户端配置类
@Resource
private ClientOAuth2DataConfiguration clientOAuth2DataConfiguration;
// 认证管理
@Resource
private AuthenticationManager authenticationManager;
// 将 tokne 存储至 redis
@Resource
private RedisTokenStore redisTokenStore;
// 登录校验
@Resource
private UserService userService;
/**
* 配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许访问 token 的公钥,默认 /oauth/token_key 是受保护的
security.tokenKeyAccess("permitAll()")
// 允许检查 token 状态,默认 /oauth/check_token 是受保护的
.checkTokenAccess("permitAll()");
}
/**
* 客户端配置 - 授权管理
*
* @param clients
* @throws Exception
*/
@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());
}
/**
* 配置授权以及令牌的访问端点和令牌服务
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
// 登录校验
.userDetailsService(userService)
// token 存储的方式:内存、redis、数据库、jwt 等
.tokenStore(redisTokenStore)
// 令牌增强对象,增强返回的结果
.tokenEnhancer((accessToken, authentication) -> {
// 获取登录后返回的结果,然后设置
SignInIdentity identity = (SignInIdentity) authentication.getPrincipal();
Map<String, Object> map = new LinkedHashMap<>();
map.put("nickname", identity.getNickname());
map.put("avatarUrl", identity.getAvatarUrl());
// 设置令牌增强的数据
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
token.setAdditionalInformation(map);
return token;
});
}
}
客户端配置类
package com.itkaka.oauth2.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 客户端配置类
*/
@ConfigurationProperties(prefix = "client.oauth2")
@Component
@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;
}
登录逻辑
公共实体类
package com.itkaka.commons.model.base;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
/**
* 实体对象公共属性
*/
@Getter
@Setter
public class BaseModel implements Serializable {
private Integer id;
private Date createDate;
private Date updateDate;
private int isValid;
}
食客实体类
package com.itkaka.commons.model.pojo;
import com.itkaka.commons.model.base.BaseModel;
import lombok.Getter;
import lombok.Setter;
/**
* 食客实体类
*/
@Getter
@Setter
public class Diners extends BaseModel {
// 用户名
private String username;
// 昵称
private String nickname;
// 密码
private String password;
// 手机号
private String phone;
// 邮箱
private String email;
// 头像
private String avatarUrl;
// 角色
private String roles;
}
DinersMapper
package com.itkaka.diners.mapper;
import com.itkaka.commons.model.dto.DinersDTO;
import com.itkaka.commons.model.pojo.Diners;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 食客 Mapper
*/
public interface DinersMapper {
/**
* 根据手机号查询食客信息
*
* @param phone
* @return
*/
@Select("select id, username, phone, email, is_valid " +
" from t_diners where phone = #{phone}")
Diners selectByPhone(@Param("phone") String phone);
/**
* 根据用户名查询食客信息
*
* @param username
* @return
*/
@Select("select id, username, phone, email, is_valid " +
" from t_diners where username = #{username}")
Diners selectByUsername(@Param("username") String username);
}
DinersService
package com.itkaka.diners.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.dto.DinersDTO;
import com.itkaka.commons.model.pojo.Diners;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.diners.config.OAuthClientConfiguration;
import com.itkaka.diners.config.OAuthClientConfiguration;
import com.itkaka.diners.mapper.DinersMapper;
import com.itkaka.diners.model.domain.OAuthDinerInfo;
import com.itkaka.diners.model.vo.LoginDinerInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 食客服务业务逻辑层
*/
@Service
public class DinersService {
@Resource
private RestTemplate restTemplate;
@Value("${service.name.fs_oauth-server}")
private String oauthServerName;
@Resource
private OAuthClientConfiguration oAuthClientConfiguration;
@Resource
private DinersMapper dinersMapper;
/**
* 登录
*
* @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(oAuthClientConfiguration));
// 设置Authorization
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// 设置BasicAuthorization
restTemplate.getInterceptors().add(
new BasicAuthenticationInterceptor(oAuthClientConfiguration.getClientId(),
oAuthClientConfiguration.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;
}
OAuthDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
new OAuthDinerInfo(), false);
LoginDinerInfo loginDinerInfo = new LoginDinerInfo();
loginDinerInfo.setToken(dinerInfo.getAccessToken());
loginDinerInfo.setNickname(dinerInfo.getNickname());
loginDinerInfo.setAvatarUrl(dinerInfo.getAvatarUrl());
return ResultInfoUtil.buildSuccess(path, loginDinerInfo);
}
/**
* 校验手机号是否已注册
*
* @param phone
*/
public void checkPhoneIsRegistered(String phone) {
AssertUtil.isNotEmpty(phone, "手机号不能为空");
Diners diners = dinersMapper.selectByPhone(phone);
AssertUtil.isTrue(diners == null, "该手机号未注册");
AssertUtil.isTrue(diners.getIsValid() == 0, "该用户已锁定,请先解锁");
}
/**
* 用户注册
*
* @param dinersDTO
* @param path
* @return
*/
public ResultInfo register(DinersDTO dinersDTO, String path) {
// 参数非空校验
String username = dinersDTO.getUsername();
AssertUtil.isNotEmpty(username, "请输入用户名");
String password = dinersDTO.getPassword();
AssertUtil.isNotEmpty(username, "请输入密码");
String phone = dinersDTO.getPhone();
AssertUtil.isNotEmpty(username, "请输入手机号");
String verifyCode = dinersDTO.getVerifyCode();
AssertUtil.isNotEmpty(username, "请输入验证码");
// 获取验证码
String code = sendVerifyCodeService.getCodeByPhone(phone);
// 验证码是否已过期
AssertUtil.isNotEmpty(code, "验证码已过期,请重新发送");
// 校验验证码一致性
AssertUtil.isTrue(!dinersDTO.getVerifyCode().equals(code),
"验证码不一致,请重新输入");
// 验证用户名是否已注册
Diners diners = dinersMapper.selectByUsername(username.trim());
AssertUtil.isTrue(diners != null, "用户名已存在,请重新输入");
// 注册
// 密码加密
dinersDTO.setPassword(DigestUtil.md5Hex(password.trim()));
dinersMapper.save(dinersDTO);
// 自动登录
return signIn(username.trim(), password.trim(), path);
}
}
启动类
package com.itkaka.diners;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.itkaka.diners.mapper")
@SpringBootApplication
public class DinersApplication {
public static void main(String[] args) {
SpringApplication.run(DinersApplication.class,args);
}
}
测试 :
访问时候 需要设置 Basic Auth 和 Body 请求体信息
返回结果:
{
"access_token": "2a83a5d3-9cc7-4017-bba2-1eb1db2d7474",
"token_type": "bearer",
"refresh_token": "c1bb81ab-252e-40de-953e-d955d0c5d51e",
"expire_in": 3599,
"scopes": "api"
}
重构
重构认证端点返回结果
Oauth控制器
package com.itkaka.oauth2.controller;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.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> params)
throws HttpRequestMethodNotSupportedException{
return custom(tokenEndpoint.postAccessToken(principal,params).getBody());
}
// 自定义 Token 返回对象
private ResultInfo custom(OAuth2AccessToken body) {
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) body;
// 获取授权服务自定义令牌增强数据
Map<String,Object> data = new LinkedHashMap<>(token.getAdditionalInformation());
data.put("accessToken",token.getValue());
data.put("expiresIn",token.getExpiresIn());
if (token.getRefreshToken() != null){
data.put("refreshToken",token.getRefreshToken().getValue());
}
data.put("scope",token.getScope());
return ResultInfoUtil.buildSuccess(request.getServletPath(),data);
}
}
重构登录逻辑,增强令牌返回结果
登录认证对象
package com.itkaka.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;
/**
* 登录认证对象
*/
@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;
}
}
UserService
package com.itkaka.oauth2.service;
import com.itkaka.commons.model.domain.SignInIdentity;
import com.itkaka.commons.model.pojo.Diners;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.oauth2.mapper.DinersMapper;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* 登录校验
*/
@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("用户名或者密码错误,请重新输入!");
}
// 初始化登录认证对象
SignInIdentity signInIdentity = new SignInIdentity();
// 拷贝属性
BeanUtils.copyProperties(diners,signInIdentity);
return signInIdentity;
}
}
增强令牌返回信息
package com.itkaka.oauth2.config;
import com.itkaka.commons.model.domain.SignInIdentity;
import com.itkaka.oauth2.service.UserService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 授权服务
*/
@EnableAuthorizationServer
@Configuration
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// 密码编码器
@Resource
private PasswordEncoder passwordEncoder;
// 客户端配置类
@Resource
private ClientOAuth2DataConfiguration clientOAuth2DataConfiguration;
// 认证管理
@Resource
private AuthenticationManager authenticationManager;
// 将 tokne 存储至 redis
@Resource
private RedisTokenStore redisTokenStore;
// 登录校验
@Resource
private UserService userService;
/**
* 配置令牌端点的安全约束
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许访问 token 的公钥,默认 /oauth/token_key 是受保护的
security.tokenKeyAccess("permitAll()")
// 允许检查 token 状态,默认 /oauth/check_token 是受保护的
.checkTokenAccess("permitAll()");
}
/**
* 客户端配置 - 授权管理
*
* @param clients
* @throws Exception
*/
@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());
}
/**
* 配置授权以及令牌的访问端点和令牌服务
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
// 登录校验
.userDetailsService(userService)
// token 存储的方式:内存、redis、数据库、jwt 等
.tokenStore(redisTokenStore)
// 令牌增强对象,增强返回的结果
.tokenEnhancer((accessToken, authentication) -> {
// 获取登录后返回的结果,然后设置
SignInIdentity identity = (SignInIdentity) authentication.getPrincipal();
Map<String, Object> map = new LinkedHashMap<>();
map.put("nickname", identity.getNickname());
map.put("avatarUrl", identity.getAvatarUrl());
// 设置令牌增强的数据
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
token.setAdditionalInformation(map);
return token;
});
}
}
测试
访问时设置 Basic Auth 和 Body 请求体信息。
返回结果:
{
"code": 1,
"message": "Successful.",
"path": "/oauth/token",
"data": {
"nickname": "test",
"avatarUrl": "/123",
"accessToken": "dc7c8ba6-efe2-4f80-afec-27dda52f4409",
"expireIn": 3579,
"scopes": [
"api"
],
"refreshToken": "01f9db59-7416-400f-9f7b-d9ee81ad37e5"
}
}
登录
fs_diners 子项目
配置文件
<?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>food_social</artifactId>
<groupId>com.itkaka</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>fs_diners</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<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>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- spring data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- commons 公共项目 -->
<dependency>
<groupId>com.itkaka</groupId>
<artifactId>fs_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>
<!-- 集中定义项目所需插件 -->
<build>
<plugins>
<!-- spring boot maven 项目打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
server:
port: 8091 # 端口
spring:
application:
name: fs_diners # 应用名
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_lezijie_food_social?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: root
# Redis
# redis:
# port: 6379
# host: 192.168.10.101
# timeout: 3000
# password: 123456
# swagger
swagger:
base-package: com.itkaka.diners
title: 美食社交食客API接口文档
# Oauth2 客户端信息
oauth2:
client:
client-id: appId
secret: 123456
grant_type: password
scope: api
# oauth2 服务地址
service:
name:
fs_oauth-server: http://fs_oauth/
fs_points-server: http://fs_points/
# Mybatis
mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰映射
# 配置 Eureka Server 注册中心
eureka:
instance:
# 开启 ip 注册
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8090/eureka/
# 配置日志
logging:
pattern:
console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'
客户端配置类
package com.itkaka.diners.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 客户端配置类
*/
@Component
@ConfigurationProperties(prefix = "oauth2.client")
@Getter
@Setter
public class OAuthClientConfiguration {
private String clientId;
private String secret;
private String grant_type;
private String scope;
}
Rest配置类
package com.itkaka.diners.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* Rest 配置类
*/
@Configuration
public class RestTemplateConfiguration {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
登录逻辑
DinersService
package com.itkaka.diners.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.dto.DinersDTO;
import com.itkaka.commons.model.pojo.Diners;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.diners.config.OAuthClientConfiguration;
import com.itkaka.diners.config.OAuthClientConfiguration;
import com.itkaka.diners.mapper.DinersMapper;
import com.itkaka.diners.model.domain.OAuthDinerInfo;
import com.itkaka.diners.model.vo.LoginDinerInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 食客服务业务逻辑层
*/
@Service
public class DinersService {
@Resource
private RestTemplate restTemplate;
@Value("${service.name.fs_oauth-server}")
private String oauthServerName;
@Resource
private OAuthClientConfiguration oAuthClientConfiguration;
@Resource
private DinersMapper dinersMapper;
/**
* 登录
*
* @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(oAuthClientConfiguration));
// 设置Authorization
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
// 设置BasicAuthorization
restTemplate.getInterceptors().add(
new BasicAuthenticationInterceptor(oAuthClientConfiguration.getClientId(),
oAuthClientConfiguration.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;
}
OAuthDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
new OAuthDinerInfo(), false);
LoginDinerInfo loginDinerInfo = new LoginDinerInfo();
loginDinerInfo.setToken(dinerInfo.getAccessToken());
loginDinerInfo.setNickname(dinerInfo.getNickname());
loginDinerInfo.setAvatarUrl(dinerInfo.getAvatarUrl());
return ResultInfoUtil.buildSuccess(path, loginDinerInfo);
}
}
package com.itkaka.diners.controller;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.dto.DinersDTO;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.diners.service.DinersService;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
/**
* 食客服务控制层
*/
@RestController
@Api(tags = "食客相关接口")
public class DinersController {
@Resource
private DinersService dinersService;
@Resource
private HttpServletRequest request;
/**
* 登录
*
* @param account
* @param password
* @return
*/
@GetMapping("signin")
public ResultInfo signIn(String account, String password) {
return dinersService.signIn(account, password, request.getServletPath());
}
}
实体类
package com.itkaka.diners.model.domain;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.List;
@Getter
@Setter
public class OAuthDinerInfo implements Serializable {
private String nickname;
private String avatarUrl;
private String accessToken;
private String expiresIn;
private List<String> scope;
private String refreshToken;
}
package com.itkaka.diners.model.vo;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Setter
@Getter
public class LoginDinerInfo implements Serializable {
private String nickname;
private String token;
private String avatarUrl;
}
启动类
package com.itkaka.diners;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.itkaka.diners.mapper")
@SpringBootApplication
public class DinersApplication {
public static void main(String[] args) {
SpringApplication.run(DinersApplication.class,args);
}
}
测试
访问:http://localhost:8091/signin?account=test&password=123456
返回结果
{
"code": 1,
"message": "Successful.",
"path": "/signin",
"data": {
"nickname": "test",
"token": "74cb18de-0645-4f3b-96bc-df6856eaee31",
"avatarUrl": "/123"
}
}
获取当前用户
用户登录信息对象
fs_commons 公共项目添加用户登录信息对象
package com.itkaka.commons.model.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@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;
}
用户中心
fs_oauth 认证授权中心修改添加: 成为如下代码
package com.itkaka.oauth2.controller;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.domain.SignInIdentity;
import com.itkaka.commons.model.vo.SignInDinerInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* 用户中心
*/
@RestController
@RequestMapping("user")
public class UserController {
@Resource
private HttpServletRequest request;
@Resource
private RedisTokenStore redisTokenStore;
//获取当前用户
@GetMapping("me")
public ResultInfo getCurrentUser(Authentication authentication){
SignInIdentity identity = (SignInIdentity) authentication.getPrincipal();
// 转为前台可用的 VO 对象
SignInDinerInfo dinerInfo = new SignInDinerInfo();
BeanUtils.copyProperties(identity,dinerInfo);
return ResultInfoUtil.buildSuccess(request.getServletPath(),dinerInfo);
}
//安全退出
@GetMapping("logout")
public ResultInfo<String> logout(String access_token,
@RequestHeader(value = "Authorization",required = false) 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);
redisTokenStore.removeAccessTokenUsingRefreshToken(refreshToken);
}
return ResultInfoUtil.buildSuccess(request.getServletPath(),"退出成功!");
}
}
认证失败处理
package com.itkaka.oauth2.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* 认证失败处理
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Resource
private ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
// 返回 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 resultInfo = ResultInfoUtil.buildError(ApiConstant.ERROR_CODE,
errorMessage, request.getRequestURI());
out.write(objectMapper.writeValueAsString(resultInfo));
out.flush();
out.close();
}
}
资源服务
package com.itkaka.oauth2.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import javax.annotation.Resource;
/**
* 资源服务
*/
@Configuration
@EnableResourceServer
public class ResourcesServerConfig 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);
}
}
测试:
通过登录逻辑调用认证中心生成 token
然后测试用户中心是否可用
获取当前用户信息
访问
安全退出
网关配置
配置文件
server:
port: 80 # 端口
spring:
application:
name: fs_gateway # 应用名
cloud:
gateway:
discovery:
locator:
enabled: true # 开启配置注册中心进行路由功能
lower-case-service-id: true # 将服务名称转小写
routes:
- id: fs_diners
uri: lb://fs_diners
predicates:
- Path=/diners/**
filters:
- StripPrefix=1
- id: fs_oauth
uri: lb://fs_oauth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
# 配置 Eureka Server 注册中心
eureka:
instance:
# 开启 ip 注册
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8090/eureka/
# 配置日志
logging:
pattern:
console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'
白名单
package com.itkaka.gateway.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 网关白名单配置
*/
@Data
@ConfigurationProperties(prefix = "secure.ignore")
@Component
public class IgnoreUrlsConfig {
private List<String> urls;
}
Rest配置类
package com.itkaka.gateway.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfiguration {
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
登录返回处理
package com.itkaka.gateway.component;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.nio.charset.Charset;
//登录返回处理
@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);
if (StringUtils.isBlank(error)) {
error = ApiConstant.NO_LOGIN_MESSAGE;
}
ResultInfo resultInfo = ResultInfoUtil.buildError(ApiConstant.NO_LOGIN_CODE, error, 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));
}
}
全局过滤器
package com.itkaka.gateway.filter;
import com.itkaka.gateway.component.HandleException;
import com.itkaka.gateway.config.IgnoreUrlsConfig;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
/**
* 网关全局过滤器
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Resource
private HandleException handleException;
@Resource
private RestTemplate restTemplate;
@Resource
private IgnoreUrlsConfig ignoreUrlsConfig;
/**
* 身份验证,拦截处理
*
* @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 pattern : ignoreUrlsConfig.getUrls()) {
if (pathMatcher.match(pattern, path)) {
flag = true;
break;
}
}
// 白名单放行
if (flag) {
return chain.filter(exchange);
}
// 获取 access_token
String token = exchange.getRequest().getQueryParams().getFirst("access_token");
// 判断 access_token 是否为空
if (StringUtils.isBlank(token)) {
return handleException.writeError(exchange, "请登录");
}
// 校验 token,拼接请求地址
String checkTokenUrl = "http://fs-oauth/oauth/check_token?token=".concat(token);
// 发送远程请求,验证 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(token));
}
if (StringUtils.isBlank(entity.getBody())) {
return handleException.writeError(exchange,
"Token was not recognised, token: ".concat(token));
}
} catch (Exception e) {
return handleException.writeError(exchange,
"Token was not recognised, token: ".concat(token));
}
// 放行
return chain.filter(exchange);
}
/**
* 排序,数字越小,越前执行
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
测试
访问:http://localhost/diners/signin?account=test&password=123456
访问:http://localhost/auth/user/me?access_token=
访问:http://localhost/auth/user/logout?access_token=
总结流程图
写在最后:
:::info
单点登录
我们使用 Spring Security 和 OAuth2 实现了授权认证中心及单点登录的功能。
这个功能中 Redis 主要用于存储 Token 令牌信息,使用了 String 数据类型
:::