spring cloud alibaba开发笔记四(授权、鉴权中心微服务JWT)

JWT的基本概念

◆JSON Web Token (JWT)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。

那些场景使用:会话状态保持,用户授权,信息交换。

JWT的结构及其含义

◆JWT由三个部分组成: Header(头部)、Payload(数据JSON)、 Signature(签名) ,且用圆点连接xxxx.yyy.zzzz

◆Header :由两部分( Token类型、加密算法名称)组成,并使用Base64编码,例:

{

        'alg': "HS256",   //签名的算法

        'typ': "JWT"     //令牌的类型

}

◆Payload : KV形式的数据,即你想传递的数据(授权的话就是Token信息)

◆Signature :为了得到签名部分,你必须有编码过的Header、 编码过的payload、 一个秘钥,签名算法是Header中指定的那个,然对它们签名即可。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

搭建授权,鉴权微服务:首先是授权方面

同样的步骤,先创建子模块:

pom.xml示例

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>e-commerce-springcloud</artifactId>
        <groupId>com.taluohui.ecommerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>e-commerce-authority-center</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>e-commerce-authority-center</name>
    <description>授权中心</description>

    <dependencies>
        <!-- spring cloud alibaba nacos discovery 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.3.RELEASE</version>
        </dependency>
        <!-- Java Persistence API, ORM 规范 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- MySQL 驱动, 注意, 这个需要与 MySQL 版本对应 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.12</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.taluohui.ecommerce</groupId>
            <artifactId>e-commerce-mvc-config</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!-- screw 生成数据库文档 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>cn.smallbun.screw</groupId>
            <artifactId>screw-core</artifactId>
            <version>1.0.3</version>
        </dependency>
    </dependencies>

    <!--
        SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
        SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
     -->
    <build>
        <finalName>${artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

配置文件bootstrap.yml示例

server:
  port: 7000
  servlet:
    context-path: /ecommerce-authority-center

spring:
  application:
    name: e-commerce-authority-center
  cloud:
    nacos:
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        server-addr: 127.0.0.1:8848 # Nacos 服务器地址
        # server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址
        namespace: 22d40198-8462-499d-a7fe-dbb2da958648
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
    properties:
      hibernate.show_sql: true
      hibernate.format_sql: true
    open-in-view: false
  datasource:
    # 数据源
    url: jdbc:mysql://127.0.0.1:3306/ecommerce?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: shuai
    password: shuai
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池
    hikari:
      maximum-pool-size: 8
      minimum-idle: 4
      idle-timeout: 30000
      connection-timeout: 30000
      max-lifetime: 45000
      auto-commit: true
      pool-name: ImoocEcommerceHikariCP
  kafka:
    bootstrap-servers: 127.0.0.1:9092
    producer:
      retries: 3
    consumer:
      auto-offset-reset: latest
  zipkin:
    sender:
      type: kafka # 默认是 web
    base-url: http://127.0.0.1:9411/

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

启动项AuthorityCenterApplication

/**
 * <h1>授权中心启动入口</h1>
 */
@EnableJpaAuditing   //允许Jpa的自动审计
@EnableDiscoveryClient
@SpringBootApplication
public class AuthorityCenterApplication {

    public static void main(String[] args) {

        SpringApplication.run(AuthorityCenterApplication.class, args);
    }
}

实体类entity/EcommerceUser(需要自己创建数据库,和对应的表)

/**
 * <h1>用户表实体类定义</h1>
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@EntityListeners(AuditingEntityListener.class)
@Table(name = "t_ecommerce_user")
public class EcommerceUser implements Serializable {

    /** 自增主键 */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    /** 用户名 */
    @Column(name = "username", nullable = false)
    private String username;

    /** MD5 密码 */
    @Column(name = "password", nullable = false)
    private String password;

    /** 额外的信息, json 字符串存储 */
    @Column(name = "extra_info", nullable = false)
    private String extraInfo;

    /** 创建时间 */
    @CreatedDate
    @Column(name = "create_time", nullable = false)
    private Date createTime;

    /** 更新时间 */
    @LastModifiedDate
    @Column(name = "update_time", nullable = false)
    private Date updateTime;
}

dao/EcommerceUserDao接口类

/**
 * <h1>EcommerceUser Dao 接口定义</h1>
 */
public interface EcommerceUserDao extends JpaRepository<EcommerceUser, Long> {
    /**
     * <h2>根据用户名查询 EcommerceUser 对象</h2>
     * select * from t_ecommerce_user where username = ?
     */
    EcommerceUser findByUsername(String username);

    /**
     * <h2>根据用户名和密码查询 EcommerceUser 对象</h2>
     * select * from t_ecommerce_user where username = ? and where password = ?
     */
    EcommerceUser findByUsernameAndPassword(String username, String password);
}

测试

在test文件下创建包com.taluohui.ecommerce,添加测试类AuthorityCenterApplicationTests 

/**
 * <h1>授权中心测试入口</h1>
 * 验证授权中心环境可用性
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class AuthorityCenterApplicationTests {
    @Test
    public void contextLoad() {
    }
}

创建包service,创建测试类

/**
 * <h1>EcommerceUser 相关的测试</h1>
 */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class EcommerceUserTest {

    @Autowired
    private EcommerceUserDao ecommerceUserDao;

    @Test
    public void createUserRecord() {

        EcommerceUser ecommerceUser = new EcommerceUser();
        ecommerceUser.setUsername("shuai");
        ecommerceUser.setPassword(MD5.create().digestHex("12345678"));
        ecommerceUser.setExtraInfo("{}");
        log.info("save user: [{}]",
                JSON.toJSON(ecommerceUserDao.save(ecommerceUser)));
    }
}

生成RSA256公钥私钥对

可以通过以下方法生成公钥和私钥,用于之后的加密

/**
 * <h1>RSA 非对称加密算法:生成公钥和私钥</h1>
 */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class RSATest {

    @Test
    public void generateKeyBytes() throws Exception {

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);

        //生成公钥和私钥对
        KeyPair keyPair = keyPairGenerator.generateKeyPair();
        //获取公钥和私钥对象
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        log.info("private key: [{}]", Base64.encode(privateKey.getEncoded()));
        log.info("public key: [{}]", Base64.encode(publicKey.getEncoded()));
    }
}

将私钥保存在鉴权模块的常量信息中

在鉴权模块中创建包constant,并创建AuthorityConstant 

/**
 * <h1>授权需要使用的一些常量信息</h1>
 */
public final class AuthorityConstant {

    /** RSA私钥,除了授权中心以外,不暴露给任何的客户端 */
    public static final String PRIVATE_KEY = "私钥,特别长";

    /** 默认的 Token 超时时间,一天 */
    public static final Integer DEFAULT_EXPIRE_DAY= 1;

}

将公钥保存在通用模块的常量信息中

在通用模块中创建包constant,并创建CommonConstant 

/**
 * <h1>通用模块常量定义</h1>
 */
public final class CommonConstant {

    /** RSA公钥 */
    public static final String PUBLIC_KEY = "公钥,不是很长";

    /** JWT 中存储用户信息的key */
    public static final String JWT_USER_INFO_KEY = "e-commerce-user";

    /** 授权中心的 service-id */
    public static final String AUTHORITY_CENTER_SERVICE_ID = "e-commerce-authority-center";
}

定义一些用于鉴权中心的对象

在通用模块vo包下中定义一些对象

/**
 * <h1>用户名和密码</h1>
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsernameAndPassword {

    /** 用户名 */
    private String username;

    /** 密码 */
    private String password;
}
/**
 * <h1>授权中心鉴权之后给客户端的 Token</h1>
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtToken {

    /** JWT */
    private String token;
}
/**
 * <h1>登录用户信息</h1>
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserInfo {

    /** 用户 id */
    private Long id;

    /** 用户名 */
    private String username;
}

基于JWT+RSA256的授权

JWT 相关服务接口定义

主要的三个共功能:

  1. 生成 JWT Token,使用默认的超时时间
  2. 生成指定超时时间的 Token, 单位是天
  3. 注册用户并生成 Token 返回

完成service方面的代码

/**
 * <h1>JWT 相关服务接口定义</h1>
 */
public interface IJWTService {

    /**
     * <h2>生成 JWT Token,使用默认的超时时间</h2>
     */
    String generateToken(String username, String password) throws Exception;

    /**
     * <h2>生成指定超时时间的 Token, 单位是天</h2>
     */
    String generateToken(String username, String password, int expire) throws Exception;

    /**
     * <h2>注册用户并生成 Token 返回</h2>
     */
    String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword) throws Exception;
}

具体实现impl/JWTServiceImpl

@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
public class JWTServiceImpl implements IJWTService {

    private final EcommerceUserDao ecommerceUserDao;

    public JWTServiceImpl(EcommerceUserDao ecommerceUserDao) {
        this.ecommerceUserDao = ecommerceUserDao;
    }

    @Override
    public String generateToken(String username, String password) throws Exception {
        return generateToken(username, password, 0);
    }

    @Override
    public String generateToken(String username, String password, int expire) throws Exception {

        //首先需要验证用户是否能够通过授权校验,即输入的用户名和密码能否匹配数据表记录
        EcommerceUser ecommerceUser = ecommerceUserDao.findByUsernameAndPassword(username, password);

        if (null == ecommerceUser) {
            log.error("can not find user: [{}}, [{}]", username, password);
            return null;
        }

        //Token中塞入对象,即JWT中存储的信息,后端拿到这些信息就可以知道是哪个用户在操作
        LoginUserInfo loginUserInfo = new LoginUserInfo(ecommerceUser.getId(), ecommerceUser.getUsername());

        if(expire <= 0) {
            expire = AuthorityConstant.DEFAULT_EXPIRE_DAY;
        }

        //计算超时时间
        ZonedDateTime zdt = LocalDate.now().plus(expire, ChronoUnit.DAYS)
                .atStartOfDay(ZoneId.systemDefault());
        Date expireDate = Date.from(zdt.toInstant());

        return Jwts.builder()
                // jwt payload --> KV K:JWT_USER_INFO_KEY V:用户信息
                .claim(CommonConstant.JWT_USER_INFO_KEY, JSON.toJSONString(loginUserInfo))
                //jwt id
                .setId(UUID.randomUUID().toString())
                //jwt 过期时间
                .setExpiration(expireDate)
                //jwt 签名 --> 加密
                .signWith(getPrivateKey(), SignatureAlgorithm.RS256)
                .compact();
    }

    @Override
    public String registerUserAndGenerateToken(UsernameAndPassword usernameAndPassword) throws Exception {

        //先去校验用户名是否存在,如果存在,不能重复注册
        EcommerceUser oldUser = ecommerceUserDao.findByUsername(
                usernameAndPassword.getUsername());

        if(null != oldUser) {
            log.error("username is registered: [{}]", oldUser.getUsername());
            return null;
        }

        EcommerceUser ecommerceUser = new EcommerceUser();
        ecommerceUser.setUsername(usernameAndPassword.getUsername());
        ecommerceUser.setPassword(usernameAndPassword.getPassword());     // MD5 编码以后
        ecommerceUser.setExtraInfo("{}");

        //注册一个新用户,写一条记录到数据表中
        ecommerceUser = ecommerceUserDao.save(ecommerceUser);
        log.info("register user success: [{}], [{}]", ecommerceUser.getUsername(),
                ecommerceUser.getId());

        //生成 token 并返回
        return generateToken(ecommerceUser.getUsername(), ecommerceUser.getPassword());
    }

    private PrivateKey getPrivateKey() throws Exception {

        PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(
                new BASE64Decoder().decodeBuffer(AuthorityConstant.PRIVATE_KEY));
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        return keyFactory.generatePrivate(priPKCS8);
    }
}

对外暴露的授权服务controller

实际上就是登录和注册的接口

/**
 * <h1>对外暴露的授权服务接口</h1>
 */
@Slf4j
@RestController
@RequestMapping("/authority")
public class AuthorityController {

    private final IJWTService ijwtService;

    public AuthorityController(IJWTService ijwtService) {
        this.ijwtService = ijwtService;
    }

    /**
     * <h2>从授权中心获取Token(其实就是登录功能),且返回信息中没有统一响应的包装</h2>
     */
    @IgnoreResponseAdvice
    @PostMapping("/token")
    public JwtToken token(@RequestBody UsernameAndPassword usernameAndPassword) throws Exception {

        log.info("request to get token with param: [{}]",
                JSON.toJSONString(usernameAndPassword));
        return new JwtToken(ijwtService.generateToken(
                usernameAndPassword.getUsername(),
                usernameAndPassword.getPassword()
        ));
    }

    /**
     * <h2>注册用户并返回当前注册用户的Token,即通过授权中心创建用户</h2>
     */
    @IgnoreResponseAdvice
    @PostMapping("/register")
    public JwtToken register(@RequestBody UsernameAndPassword usernameAndPassword) throws Exception {

        log.info("request user with param: [{}]", JSON.toJSONString(usernameAndPassword));
        return new JwtToken(ijwtService.registerUserAndGenerateToken(usernameAndPassword));
    }
}

鉴权功能的开发

在通用模块中添加工具包以及鉴权工具类TokenParseUtil

就是将JWT中的用户id和用户名解析出来

对于公钥私钥可以保存在文件服务器中,这样更安全一些

/**
 * <h1>JWT Token 解析工具类</h1>
 */
public class TokenParseUtil {

    /**
     *<h2>从 JWT Token 中解析LoginUserInfo对象</h2>
     */
    public static LoginUserInfo parseUserInfoFromToken(String token) throws Exception {
        if(null == token) {
            return null;
        }
        Jws<Claims> claimsJws = parseToken(token, getPublicKey());
        Claims body = claimsJws.getBody();

        //如果 Token 已经过期了,返回null
        if (body.getExpiration().before(Calendar.getInstance().getTime())) {
            return null;
        }

        //返回Token中保存的用户信息
        return JSON.parseObject(
                body.get(CommonConstant.JWT_USER_INFO_KEY).toString(),
                LoginUserInfo.class
        );
    }

    /**
     *<h2>通过公钥去解析 JWT Token</h2>
     */
    public static Jws<Claims> parseToken(String token, PublicKey publicKey) {
            return Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
    }

    /**
     *<h2>根据本地存储的公钥获取到 PublicKey 对象</h2>
     */
    private static PublicKey getPublicKey() throws Exception {

        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
                new BASE64Decoder().decodeBuffer(CommonConstant.PUBLIC_KEY)
        );
        return KeyFactory.getInstance("RSA").generatePublic(keySpec);
    }
}

测试授权和鉴权功能的可用性

/**
 * <h1>JWT 相关服务测试类</h1>
 */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class JWTServiceTest {

    @Autowired
    private IJWTService ijwtService;

    @Test
    public void testGenerateAndParseToken() throws Exception {

        String jwtToken = ijwtService.generateToken(
                "shuai",
                "25d55ad283aa400af464c76d713c07ad"
        );
        log.info("jwt token is: [{}]", jwtToken);

        LoginUserInfo userInfo = TokenParseUtil.parseUserInfoFromToken(jwtToken);
        log.info("parse token: [{}]", JSON.toJSONString(userInfo));

    }
}
###获取 Token ---- 登录功能实现
POST http://localhost:7000/ecommerce-authority-center/authority/token
Content-Type: application/json

{
  "username": "shuai",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

###注册用户并返回Token ---- 注册功能实现
POST http://localhost:7000/ecommerce-authority-center/authority/register
Content-Type: application/json

{
  "username": "shuai1we",
  "password": "25d55ad283aa400af464c76d713c07ad"
}

screw开源工具 生成数据库文档

依赖

        <!-- screw 生成数据库文档 -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.30</version>
        </dependency>
        <dependency>
            <groupId>cn.smallbun.screw</groupId>
            <artifactId>screw-core</artifactId>
            <version>1.0.3</version>
        </dependency>

代码

/**
 * <h1>数据库表文档生成</h1>
 */
@SpringBootTest
@RunWith(SpringRunner.class)
public class DBDocTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void buildDBDoc() {

        DataSource dataSourceMysql = applicationContext.getBean(DataSource.class);

        EngineConfig engineConfig = EngineConfig.builder()
                //生成文件路径
                .fileOutputDir("/Users/duzin/IdeaProjects/e-commerce-springcloud")
                //生成之后是否自动打开
                .openOutputDir(false)
                //文件类型
                .fileType(EngineFileType.HTML)
                .produceType(EngineTemplateType.freemarker)
                .build();

        //生成文档配置,包含自定义版本号、描述等等
        Configuration config = Configuration.builder()
                //版本号,随便给
                .version("1.0.0")
                //工程描述
                .description("e-commerce-springcloud")
                //数据源配置
                .dataSource(dataSourceMysql)
                //引擎的配置
                .engineConfig(engineConfig)
                //想要生成想要忽略哪些表的配置
                .produceConfig(getProduceConfig())
                .build();

        //执行生成
        new DocumentationExecute(config).execute();
    }

    /**
     *<h2>配置想要生成的数据表和想要忽略的数据表</h2>
     */
    private ProcessConfig getProduceConfig() {

        //想要忽略的数据表
        List<String> ignoreTableName = Collections.singletonList("undo_log");
        //忽略表前缀, 忽略a、b开头的数据表
        List<String> ignorePrefix = Arrays.asList("a", "b");
        //忽略表后缀
        List<String> ignoreSuffix = Arrays.asList("_test", "_Test");

        return ProcessConfig.builder()
                //根据名称指定生成
                .designatedTableName(Collections.emptyList())
                //根据表前缀生成
                .designatedTablePrefix(Collections.emptyList())
                //根据表后缀生成
                .designatedTableSuffix(Collections.emptyList())
                //根据名称指定忽略
                .ignoreTableName(ignoreTableName)
                //根据表前缀忽略
                .ignoreTablePrefix(ignorePrefix)
                //根据表后缀忽略
                .ignoreTableSuffix(ignoreSuffix)
                .build();
    }
}

基于Token与基于服务器的身份认证对比

基于服务器的身份认证

◆最为传统的做法,客户端存储Cookie (一般是Session id ),服务器存储Session

◆Session 是每次用户认证通过以后,服务器需要创建一条记录保存用户信息,通常是在内存(redis)中,随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大

◆在不同域名之前切换时,请求可能会被禁止;即跨域问题

基于Token ( JWT )的身份认证

◆JWT与Session的差异相同点是,它们都是存储用户信息;然而, Session是在服务器端的,而JWT是在客户端的

◆JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力

其实可以将两者进行结合,将JWT保存在redis中。

两者优缺点的对比

◆解析方法: JWT使用算法直接解析得到用户信息; Session需要额外的数据映射,实现匹配

◆管理方法: JWT只有过期时间的限制; Session 数据保存在服务器,可控性更强

◆跨平台: JWT就是一-段字符串,可以任意传播; Session跨平台需要有统- -的解析平台,较为繁琐

◆时效性: JWT -旦生成,独立存在,很难做特殊控制; Session时效性完全由服务端的逻辑说了算

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值