软工Web实训日志-Day1-后端-准备和认证功能的初步实现

写在前面

临近期末,大三上课程实训要求用AI开发一个前后端完备的web项目,我本身是正在开发一个会上线的现代化论坛项目(虽然磨磨蹭蹭也搞了一年了,再过几天到第二年了哈哈。。)
按照2周的时间,其实时间对于开发小型的项目比较打紧的,也不可能完善所有的功能。所以我准备实现最基本的功能,以及对扩展预留一定的空间和接口。

Java模式其实对于很多小型项目来说是比较过度的,至少对于我认为是这样子的。但是对于大型的工程体系还是一目了然,当然对于一个中型项目,个人开发者来说,开发难度相比有几个人组成的开发的团队来说还是有非常高的难度,毕竟现在的服务架构体系和古早的传统架构体系已经天差地别了,这也是我磨磨蹭蹭的原因,也是多亏了AI和各路大佬包括CSDN,博客园还有Github等等,让我看到了一个个优秀的工程化的项目是怎么构建了,也认识到了很多很厉害的人。

这里及接下来的日记是对于整个实训期间我做的事情,有些观点也比较主观和片面,也欢迎各位大佬提供指定和帮助!!

项目名称:船舶管理系统

(为了避开和其他人重复,其实也烂大街了实际上,想了一个比较偏的)
本次实训是以 Java SpringBoot + vue + mysql + redis 的传统架构体系开发的。前端决定采用AI完善和优化

我决定是采用Spring Security来进行对认证的管理

Cors配置

首先配置一下令以前的我头大的,还有很多初学者非常头大的跨域配置

若是只配置前端的化,需要用到代理来实现。

这里使用Spring Web提供的跨域直接配置。
Vue 的 dev 默认是以localhost:5173

@Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedHeaders(List.of("*")); // 允许所有请求头
        config.setAllowedOrigins(List.of(
                "http://localhost:5173" // 预取跨域的源目标
        ));
        config.setAllowedMethods(List.of(
                "Get", "POST", "PUT", "DELETE", "OPTIONS" // 允许方法
        ));
        config.setAllowCredentials(true); // 允许携带凭证

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
     }

为了方便和第三方认证对接,我决定采用oauth2提供的管道实现对jwt的解析和认证,以及对认证上下文的注入。
这里为了简化开发,不引入如RBAC的权限系统架构(用户就是管理员,B端)
所以上下文当前只带一个userId,可以替换成userUid,uid用 SnowFlake算法进行生成,防止攻击者破坏性攻击。

@Getter
@RequiredArgsConstructor(staticName = "of")
public class UserContext implements UserDetails {
    private final Long userId;
    @Override
    public @NotNull Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of();
    }

    @Override
    public @Nullable String getPassword() {
        return null;
    }

    @Override
    public @NotNull String getUsername() {
        return userId.toString();
    }

    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }
}

实现 Jwt 的解析校验和与 oauth2 的 jwt 对接

为提高Jwt的签名安全性,采用非对称加密算法RSA作为Jwt的签名算法。

我采用OpenSSL 生成 RSA 的 public key 和 private key 的 key pair.

openssl rsa -pubout -in private_key.pem -out public_key.pem

生成公钥之后,再用OpenSSL工具生成对应的密钥。

openssl rsa -pubout -in private_key.pem -out public_key.pem

打开public_key.pem和private_key.pem
大概长这样:

-----BEGIN PRIVATE KEY-----
MIIEvg1...
VKtMp29...
qhaFIkn...
...
-----END PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MIIBIj...
...
...
-----END PUBLIC KEY-----

通常来讲PublicKey比较短。中间的内容(key主体)是Base64编码后的。
我们需要自己实现把生成的RSA密钥值转换成对应的Java Security 的 PublicKey 和 PrivateKey 对象

@Configuration
public class JwtKeyConfig {
    @Value("${jwt.private-Key}")
    private Resource privateKey;
    @Value("${jwt.public-Key}")
    private Resource publicKey;

    @Bean
    public PrivateKey getPrivateKey() throws Exception {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(
                extraSSHKeyBase64ContentToBytes(privateKey));
        return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
    }

    @Bean
    public PublicKey getPublicKey() throws Exception {
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
                extraSSHKeyBase64ContentToBytes(publicKey)
        );
        return KeyFactory.getInstance("RSA").generatePublic(keySpec);
    }
    // 把文件中的无关信息过滤掉,并把base64解码直接成二进制数组
    private byte[] extraSSHKeyBase64ContentToBytes(Resource res) throws IOException {
        String content = new String(res.getInputStream().readAllBytes());
        if(content.startsWith("-----BEGIN")){
            content =content.replaceAll("-----BEGIN.*?-----", "")
                    .replaceAll("-----END (.*)-----", "")
                    .replaceAll("\\s+", "");
        }
        return Base64.getDecoder().decode(content);
    }
}

(问了下AI,其实好像io.jsonwebtoken.security.Keys#parser()就可以直接把pem文件内容直接转换了...算了,写都写了,不改了)

实现解析和Token构造

@Component
public class RsaJwtUtil {
    private static final String ISSUER = "Your Issuer Here..";
    private final PublicKey publicKey;
    private final PrivateKey privateKey;

    public RsaJwtUtil(PublicKey publicKey, PrivateKey privateKey) {
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    public TokenResult buildToken(Map<String, Object> claims, Duration exp){
        Date expiration = new Date(System.currentTimeMillis() + exp.toMillis());
        return new TokenResult(Jwts.builder()
                .issuer(ISSUER)
                .claims(claims)
                .issuedAt(new Date())
                .signWith(privateKey, Jwts.SIG.RS256)
                .compact(), exp.toSeconds());
    }

    public Claims parseToken(String token){
        return Jwts.parser()
                .verifyWith(publicKey)
                .requireIssuer(ISSUER)
                .build()
                .parseSignedClaims(token)
                .getPayload();
    }
}

buildToken接受上级过来的Subject和Java token id(记录到redis实现主动过期),id同样可以用snowflake组件或者UUID生成

为了对接oatuh2,需要额外写一个针对Oauth2的 JwtDecoder 同时需要把认证后的上下文注入的类。

我们需要实现: Spring Security自动获取Authorization:Bearer token 中的string token 值,转换成 Jwt(同时对接Oauth2)

@Component
public class WaterJwtDecoder implements Converter<String, Jwt> , JwtDecoder {
    private final RsaJwtUtil rsaJwtUtil;
    private final RSAJwtTokenService tokenService;

    public WaterJwtDecoder(RsaJwtUtil rsaJwtUtil, RSAJwtTokenService tokenService) {
        this.rsaJwtUtil = rsaJwtUtil;
        this.tokenService = tokenService;
    }

    @Override
    public Jwt convert(@NotNull String token) {
            Claims claims = rsaJwtUtil.parseToken(token);
            tokenService.validateAccessTokenAndRejectOld(claims);

            return Jwt.withTokenValue(token)
                    .header("alg","RS256")
                    .header("typ","JWT")
                    .claims(c -> c.putAll(claims))
                    .issuedAt(claims.getIssuedAt().toInstant())
                    .expiresAt(claims.getExpiration().toInstant())
                    .build();
    }

    @Override
    public Jwt decode(String token) throws JwtException {
        return this.convert(token);
    }
}

第二步,把Jwt 对象转换成 Authentication 供给给 SecurityContextHolder 为后面@AuthenticationPrincipal、@PreAuthorize注解调用上下文(通常用于用户权限校验之类的)。

@Component
public class UserJwtAuthConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    @Override
    public AbstractAuthenticationToken convert(Jwt source) {
        // 这里还可以添加jwt claims中的比如role和scope,然后放到 ctx当中,给其他地方去调用。
        Long userId = Long.valueOf(source.getClaimAsString("userId"));
        UserContext ctx = UserContext.of(userId);
        UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(ctx, null, ctx.getAuthorities());
        auth.setDetails(source);
        return auth;
    }
}

Spring Security 的 SecurityFilterChain 配置

为了便于开发,我们先关闭跨域请求伪造攻击防护CSRF
后面会添加对接口的路径认证和放行的配置

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http, UserJwtAuthConverter jwtConverter) throws Exception {
        return http
                .cors(c-> c.configurationSource(corsConfigurationSource()))
                .csrf(c-> c.disable())
                .oauth2ResourceServer(
                    oauth2 ->
                            oauth2.jwt(jwt -> jwt
                                    .decoder(oauth2JwtDecoder)
                                    .jwtAuthenticationConverter(jwtConverter))
                )
                .formLogin(form -> form.disable()) // 取消 Spring Security 的默认登陆界面
                .build();

    }
完整Security的代码
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final OAuth2JwtDecoder oauth2JwtDecoder;
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, UserJwtAuthConverter jwtConverter) throws Exception {
        return http
                .cors(c-> c.configurationSource(corsConfigurationSource()))
                .csrf(c-> c.disable())
                .oauth2ResourceServer(
                    oauth2 ->
                            oauth2.jwt(jwt -> jwt
                                    .decoder(oauth2JwtDecoder)
                                    .jwtAuthenticationConverter(jwtConverter))
                )
                .formLogin(form -> form.disable())
                .build();

    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedHeaders(List.of("*"));
        config.setAllowedOrigins(List.of(
                "http://localhost:5173"
        ));
        config.setAllowedMethods(List.of(
                "Get", "POST", "PUT", "DELETE", "OPTIONS"
        ));
        config.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
     }
}

OKKKK, 今天差不多就这么多

后话

其实实际上我还是想偷懒从git上扒一个类似的下来,或者用AI写后端,但是具体细节方面我看了一下git上找的,也是用ai写的,估计也是应付老师任务,想来想去还是自己手写后端比较好,而且我个人倾向不习惯让AI去搞后端的东西,前端改改还好,AI写的快,好的地方也是好,但是还是需要自己有些地方去把关,正好温故和学习一下。。。全用AI有些好的架构和逻辑自己不去摸索,不去学,是完全不知道的。。。

posted @ 2025-12-22 23:05  Hachett  阅读(4)  评论(0)    收藏  举报
W

Waterwood

👋🏻 Hi there! 一名正在努力不被生活抛弃的大三学生

📖 软件工程专业

🧱 主要学习 Java & 后端开发

🛠️ 主要项目

VeloChatX

一个 Minecraft Velocity 服务器插件,用于分发玩家消息并提供丰富的附加功能

查看项目 →

WaterFun 🔧

一个新时代多元化互动论坛

查看项目 →

💻 技术栈

Java Spring Gradle MySQL Redis Vue.js JavaScript TypeScript Node.js HTML/CSS Git GitHub Windows PowerShell IntelliJ IDEA Nuxt.js npm

☎️ 联系方式

📊 GitHub 统计

Top Languages GitHub Stats