第二章--美食社交APP--认证授权中心

公共项目

全局常量类
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 请求体信息
image.png
image.png
返回结果:

{
  "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 请求体信息。
image.png
image.png
返回结果:

{
  "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
image.png
访问:http://localhost/auth/user/me?access_token=
image.png
访问:http://localhost/auth/user/logout?access_token=
image.png

总结流程图

image.png
image.png

写在最后:

:::info
单点登录
我们使用 Spring Security 和 OAuth2 实现了授权认证中心及单点登录的功能。
这个功能中 Redis 主要用于存储 Token 令牌信息,使用了 String 数据类型
:::

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值