spring - 微服务授权 2 实战

授权服务(微信授权服务)搭建

目录结构

PS:启动类什么也没开启

引用依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
    <version>3.2.12</version>
</dependency>

配置类

java 复制代码
package com.learn.oauth.server.config;

import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

import java.util.UUID;

@Configuration
@EnableWebSecurity
public class OauthServerConfig {

    @Resource
    private UserDetailsService userDetailsService;

    @Bean
    @Order(1)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());

        http.exceptionHandling((exceptions) -> exceptions
                        .defaultAuthenticationEntryPointFor(
                                new LoginUrlAuthenticationEntryPoint("/login"),
                                new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
                        )
                )
                .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));

        return http.build();
    }

    @Bean
    @Order(2)
    public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .cors(AbstractHttpConfigurer::disable)  // 关闭cors,影响前端 bootstrap.css .js 加载速度
                .formLogin(Customizer.withDefaults())  // 使用默认表单
                .userDetailsService(userDetailsService)  // 自定义用户
        ;

        return http.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("client")  // 某奇艺账号
                .clientSecret("{noop}secret")  // 某奇艺密码
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .redirectUri("http://127.0.0.1:8081/login/receive-code")  // 某奇艺回调地址
                .scope(OidcScopes.OPENID)
                .scope(OidcScopes.PROFILE)
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .build();

        return new InMemoryRegisteredClientRepository(oidcClient);
    }

}
java 复制代码
package com.learn.oauth.server.service;

import org.springframework.security.core.userdetails.User;
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 java.util.Set;

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 模拟数据库查询,自己随便写
        return new User(username, "{noop}123456", Set.of());
    }
}

配置文件

java 复制代码
server:
  port: 9000
客户端(某奇艺)后端服务搭建

目录结构

PS:启动类什么也没开启

引用依赖

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.38</version>
    </dependency>
</dependencies>

Controller

java 复制代码
package com.learn.oauth.client.controller;

import cn.hutool.core.lang.UUID;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/login")
public class LoginController {

    // 模拟数据库
    private final Map<String, Map<String, String>> DB = new HashMap<>();

    /**
     * 1,进入模拟登录页面
     */
    @GetMapping("/page")
    public ModelAndView page() {
        HashMap<String, String> map = new HashMap<>();
        map.put("state", UUID.fastUUID().toString());

        return new ModelAndView("page", map);
    }

    /**
     * 2,返回授权地址,前端也能拼接,但是不安全
     */
    @GetMapping("/three-login")
    public void threeLogin(@RequestParam("type") String type, @RequestParam("state") String state, HttpServletResponse response) throws IOException {
        String url;
        switch (type) {
            case "mouyin":
                // 使用某音登录
            case "mouxin":
                // 使用某信登录
            default:
                url = "http://127.0.0.1:9000/oauth2/authorize?" +
                        "response_type=code" +
                        "&client_id=client" +
                        "&state="+ state +
                        "&scope=openid%20profile" +
                        "&redirect_uri=http://127.0.0.1:8081/login/receive-code";
        }

        response.setStatus(302);
        response.sendRedirect(url);
    }

    /**
     * 3,授权服务器回调地址
     */
    @GetMapping("/receive-code")
    public void receiveCode(@RequestParam("code") String code, @RequestParam("state") String state) {
        // 获取 token
        HttpResponse tokenResponse = HttpUtil.createPost("http://127.0.0.1:9000/oauth2/token")
                .basicAuth("client", "secret")  // 模拟服务内,定义的 client_id 和 client_secret
                .form("grant_type", "authorization_code")
                .form("code", code)
                .form("redirect_uri", "http://127.0.0.1:8081/login/receive-code")  // 和上面的保持一致,会校验
                .execute();

        String accessToken = JSONUtil.parseObj(tokenResponse.body()).getStr("access_token");

        // 获取用户信息
        HttpResponse infoResponse = HttpUtil.createGet("http://127.0.0.1:9000/userinfo").bearerAuth(accessToken).execute();

        String username = JSONUtil.parseObj(infoResponse.body()).getStr("sub");

        // 放入数据库
        DB.put(state, new HashMap<>(){{
            put("username", username);
        }});
    }

    /**
     * 4,轮询是否成功
     */
    @GetMapping("/check")
    public String check(@RequestParam("state") String state) {
        return DB.containsKey(state) ? "success" : "fail";
    }

    /**
     * 5,登录成功,跳转首页
     */
    @GetMapping("/home")
    public ModelAndView home(@RequestParam("state") String state) {
        return new ModelAndView("home", DB.get(state));
    }

}

配置文件

java 复制代码
server:
  port: 8081

spring:
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html

模拟页面 page.html

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <style>
        #form {
            width: 175px;
            margin: 100px auto;
        }
        label {
            display: inline-block;
            width: 100%;
            margin-top: 20px;
        }
    </style>
</head>
<body>
<div id="form">
    <label>
        <span>用户名</span><br>
        <input type="text" name="username" placeholder="用户名"/>
    </label><br>
    <label>
        <span>密码</span><br>
        <input type="password" name="password" placeholder="密码"/>
    </label><br>
    <label>
        <button th:onclick="threeLogin('mouyin')" style="background-color: greenyellow">某音登录</button>
        <button th:onclick="threeLogin('mouxin')" style="background-color: darkorange">某信登录</button>
    </label>
</div>
<script th:inline="javascript">
    var window1;

    function threeLogin(type) {
        let url = "http://localhost:8081/login/three-login?type="+ type +"&state="+ [[${state}]] +"&_="+ new Date().getTime();
        window1 = window.open(url, '_blank', 'width=500,height=300,left=200,top=200');
    }

    setInterval(function () {
        fetch("http://localhost:8081/login/check?state="+ [[${state}]])
            .then(response => {
                return response.text();
            })
            .then(data => {
                if (data === "success") {
                    window1.close();
                    window.location.href = "http://localhost:8081/login/home?state="+ [[${state}]];
                }
            })
            .catch(error => {
                console.error(error);
            });
    }, 5000)
</script>
</body>
</html>

模拟页面 home.html

html 复制代码
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<h2>登录成功</h2>
<h3 th:text="'欢迎:'+${username}" style="color: darkorange"></h3>
</body>
</html>
相关推荐
Flittly9 小时前
【雕虫大技】Agent 动态 Skill 供应链安全加固(二):执行隔离沙箱实战
java·spring boot·spring
BUG指挥官1 天前
Sa-Token和Spring Security对比
java·后端·spring
geminigoth1 天前
Spring AI Alibaba 入门开发一(备份)
java·人工智能·spring
xbgRS1 天前
spring整合mybaits
java·spring·mybatis
ruleslol1 天前
静态依赖注入 VS 动态依赖查询
spring
墨雨晨曦882 天前
Spring AI总结
java·人工智能·spring
用户3126874877202 天前
Spring Boot 异常处理到底怎么玩的?从 DispatcherServlet 到全局兜底的全链路拆解
spring
不才不才不不才2 天前
Spring 源码系列(17): HandlerMapping 与 HandlerAdapter 两大体系
java·后端·spring
(轻舟已过万重山)2 天前
第40章 Spring AI 实战:企业级 AI 应用架构
人工智能·spring·架构
堕落年代2 天前
Ollama CPU 推理大提示词优化实测报告(细致化数据版)
java·后端·spring