前情提要

上一集,我们成功用 Postman 调通了密码模式,拿到了令牌,像极了黑客帝国里 Neo 第一次看到代码雨——兴奋又迷茫。

但是!问题来了——

如果前端直接跟授权服务器打交道,那 client-id 和 client-secret 就相当于把银行卡密码写在便利贴上,贴在显示器边框。谁路过都能瞟一眼,安全?不存在的。

今天的工程:建一座"中转站"

我们要搞一个 登录微服务,让它当前端的"传声筒"。前端只跟它唠嗑,它再去跟授权服务器勾兑。
一句话总结:前端不直接见"大佬",中间加个"经纪人"。

  1. 引入pom依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- OAuth2 资源服务器依赖 -->
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.3.9.RELEASE</version>
</dependency>

<!-- JWT 依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
  1. 编写配置文件,client:属性是用来向授权服务器用密码模式请求令牌的参数,key-uri是它本身作为oauth2的资源服务器的验证token的地址(非对称)
spring:
    application:
        name: login
server:
    port: 8124

security:
    oauth2:
        resource:
            jwt:
                key-uri: http://localhost:8129/oauth/token_key

auth:
    server:
        base:
            url: http://localhost:8129
client:
    client-id: client-app
    client-secret: secret123
  1. 创建 ResourceServerConfig 类 ,放行一个 /login 接口,用于前端登录
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/api/login").permitAll()
                .anyRequest().authenticated();
    }
}
  1. 写两个实体 ,请求和响应
@Data
public class LoginRequest {
    private String username;
    private String password;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginResponse {
    private String accessToken;
    private String refreshToken;
    private String tokenType;
    private Long expiresIn;
    private String scope;
}

创建接口测试

@RestController
@RequestMapping("/api")
public class AuthController {

    @Value("${auth.server.base.url}")
    private String authServerUrl;

    @Value("${client.client-id}")
    private String clientId;

    @Value("${client.client-secret}")
    private String clientSecret;

    private final RestTemplate restTemplate;

    public AuthController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/login")
    public LoginResponse login(@RequestBody LoginRequest loginRequest) {
        String tokenUrl = authServerUrl + "/oauth/token";

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        headers.setBasicAuth(clientId, clientSecret);

        MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type", "password");
        body.add("username", loginRequest.getUsername());
        body.add("password", loginRequest.getPassword());
        body.add("scope", "read write");

        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);

        ResponseEntity<Map> response = restTemplate.postForEntity(tokenUrl, request, Map.class);

        if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
            Map<String, Object> tokenInfo = response.getBody();

            return new LoginResponse(
                    (String) tokenInfo.get("access_token"),
                    (String) tokenInfo.get("refresh_token"),
                    (String) tokenInfo.get("token_type"),
                    ((Number) tokenInfo.get("expires_in")).longValue(),
                    (String) tokenInfo.get("scope")
            );
        } else {
            throw new RuntimeException("获取令牌失败");
        }
    }

    @GetMapping("/userinfo")
    public Map<String, Object> getUserInfo(Authentication authentication) {

        if (authentication == null || !authentication.isAuthenticated()) {
            throw new RuntimeException("未认证或Token无效");
        }

        Map<String, Object> userInfo = new HashMap<>();

        if (authentication instanceof OAuth2Authentication) {
            OAuth2Authentication oauth2Auth = (OAuth2Authentication) authentication;
            userInfo.put("username", oauth2Auth.getUserAuthentication().getName());
            userInfo.put("principal", oauth2Auth.getUserAuthentication().getPrincipal());
            userInfo.put("authorities", oauth2Auth.getUserAuthentication().getAuthorities());
            userInfo.put("clientId", oauth2Auth.getOAuth2Request().getClientId());
            userInfo.put("scope", oauth2Auth.getOAuth2Request().getScope());
            userInfo.put("grantType", oauth2Auth.getOAuth2Request().getGrantType());
        } else {
            userInfo.put("username", authentication.getName());
            userInfo.put("authorities", authentication.getAuthorities());
        }

        return userInfo;
    }
}

现在我们用 postman来测试一下
等会儿 ,授权服务器上一集写的是对称 ,我们先来改造一下 。。变成非对称

在 授权服务器中 改造一下 OAuth2AuthorizationServerConfig类的令牌转换器

//JWT 令牌转换器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

    // ✅ 2. 生成 RSA 密钥对
    KeyPair keyPair = generateRsaKeyPair();

    converter.setKeyPair(keyPair);

    return converter;
}

private KeyPair generateRsaKeyPair() {
    try {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);
        return keyPairGenerator.generateKeyPair();
    } catch (Exception e) {
        throw new RuntimeException("生成 RSA 密钥对失败", e);
    }
}

好了,现在我们来测试一下 /login接口 和 /userinfo 接口,
这时候 我们就可以用 最 熟悉的 方式 post + body(JSON)了
在这里插入图片描述
果然,已经成功获取了 令牌

现在来试试 userinfo 接口,注意 这个 接口就需要 携带token访问了。
在这里插入图片描述

总结:

在这里插入图片描述

核心思想
  • 前端只认识登录微服务(亲切友好,只说 JSON)
  • 登录微服务去跟授权服务器打交道(用 password 模式,发的是表单数据)
  • client-secret 永远不离开服务端,就像你的银行卡密码永远不会告诉别人

关于前端应用 我写了一半,有点累了 ,而且篇幅挺长,我就给删除了 ,下一集再说吧!!

下集预告

这一集,我们搞定了密码模式,前端可以通过 /api/login 拿到令牌,通过 /api/userinfo 拿到用户信息。
下一集,这回真的要创建前端应用了

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐