软工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有些好的架构和逻辑自己不去摸索,不去学,是完全不知道的。。。

浙公网安备 33010602011771号