手把手教你写项目之中小学微课学习系统(四):安全认证

阅读前请先下载项目源码,边读边看源码以加深理解和实操,
源码地址已放于文章末尾!

效果预览:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

04-安全认证-Aunt-I-don’t-want-to-work-hard-anymore!

各位小伙伴,前方高能预警!今天我们要聊的话题,是后端开发中“皇冠上的明珠”——安全认证。如果说之前的CRUD是“盖房子”,那今天我们做的就是给房子装上“防盗门”和“监控系统”。没有它,你的系统就等于是在“裸奔”,任何人都可以进来“为所欲为”。

在上一章的结尾,我留了一个巨大的“坑”:我们的用户信息,特别是密码,是明文存在数据库里的!这要是被“脱裤”,那项目负责人就得卷铺盖走人了。而且,我们的API接口也没有任何保护,谁都可以调用。这可不行!

所以,今天咱们要解决两个核心问题:

  1. 密码加密:绝不能让密码明文出现在数据库里。
  2. 接口认证:用户登录后才能访问受保护的资源,就像你得刷门禁卡才能进小区一样。

为了解决这两个问题,我们要请出两位“大神”:Spring SecurityJWT

Spring Security + JWT:后端界的“黄金搭档”

  • Spring Security: 这是Spring家族提供的专业“保安队”。它功能极其强大,能提供一整套完整的安全服务,包括认证(你是谁?)、授权(你能干啥?)、攻击防护等。但它有个毛病,就是配置起来有点复杂,像个固执的老头。
  • JWT (JSON Web Token): 这是一种轻量级的认证规范。用户登录成功后,服务器会生成一个加密的“令牌”(Token),像一张“限时通行证”,发给客户端。之后客户端每次请求需要认证的接口时,都必须带上这张“通行证”。服务器看到通行证合法,就放行。

为啥要把它俩结合起来?
Spring Security默认的认证方式是基于Session的,这在前后端分离的项目里不太好使。而JWT是无状态的,天生就适合RESTful API。所以我们用Spring Security的强大框架,但把它的核心认证逻辑换成JWT的“令牌”模式,取长补短,完美!

这个过程就像阿姨不想努力了…啊不,是《让子弹飞》里的场景:

客户端 (张三) 服务器 (黄四郎) 我是张三(zhangsan),密码123,要登录! 查数据库,发现zhangsan密码(加密后)正确 登录成功!给你一张“委任状”(JWT Token) 张三把“委-任状”揣兜里 我要看我的课程列表(带上“委任状”) 检查“委任状”是不是我发的,过没过期 没问题,给你课程列表数据 我想删库跑路(没带“委任状”) “没有委任状,你算个屁!”(401 Unauthorized) 客户端 (张三) 服务器 (黄四郎)

第一步:添加新的“兵器”

老规矩,先在 pom.xml 里加入我们需要的依赖:

<!-- ... other dependencies ... -->

<!-- Spring Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- JWT (Java JWT) -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.3</version> <!-- 使用一个较新的稳定版本 -->
</dependency>

加完之后,记得刷新一下你的Maven项目,让它把新的“兵器”下载回来。

第二步:编写JWT工具类

我们需要一个工具类来专门负责生成和校验JWT。在 com.weke.learningsystem 包下创建一个新的包 utils,然后在里面创建 JwtUtil.java

package com.weke.learningsystem.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;

public class JwtUtil {

    private static final String SECRET_KEY = "your-secret-key"; // 密钥,生产环境一定要更复杂
    private static final long EXPIRATION_TIME = 864_000_00; // 24小时

    // 生成Token
    public static String generateToken(String username) {
        return JWT.create()
                .withSubject(username)
                .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .sign(Algorithm.HMAC512(SECRET_KEY));
    }

    // 校验Token并获取用户名
    public static String validateToken(String token) {
        DecodedJWT jwt = JWT.require(Algorithm.HMAC512(SECRET_KEY))
                .build()
                .verify(token);
        return jwt.getSubject();
    }
}

注意: SECRET_KEY 是加密的密钥,非常重要,千万不要泄露!

第三步:改造用户服务(注册与登录)

现在,我们要对 UserService 进行“手术”,实现真正的注册和登录逻辑。

首先,修改 UserService 接口,增加两个新方法:

UserService.java:

// ... imports ...
public interface UserService extends IService<User> {
    User register(User user);
    String login(String username, String password);
}

然后,在 UserServiceImpl 中实现这两个方法:

UserServiceImpl.java:

// ... imports ...
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public User register(User user) {
        // 使用BCrypt对密码进行加密
        String encodedPassword = passwordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);
        this.save(user); // this.save()是ServiceImpl自带的方法
        return user;
    }

    @Override
    public String login(String username, String password) {
        User user = this.query().eq("username", username).one();
        if (user != null && passwordEncoder.matches(password, user.getPassword())) {
            // 密码匹配,生成JWT
            return JwtUtil.generateToken(username);
        }
        // 登录失败
        return null;
    }
}

代码解读:

  • BCryptPasswordEncoder: 这是Spring Security提供的密码加密工具。我们不能直接把它 new 出来,而是要把它配置成一个Bean,让Spring来管理。
  • passwordEncoder.encode(): 将用户传来的原始密码,加密成一长串谁也看不懂的字符串。
  • passwordEncoder.matches(): 用于比对用户输入的密码和数据库中加密过的密码是否匹配。

配置密码加密器Bean:

为了让 BCryptPasswordEncoder 能被 @Autowired,我们需要在主启动类 LearningSystemApplication 的同级或者上级创建一个 config 包,并在里面创建一个 SecurityConfig 类。

config/SecurityConfig.java

package com.weke.learningsystem.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

第四步:改造Controller

有了新的业务逻辑,我们的 UserController 也要与时俱进。

// ... imports ...
import java.util.Map;

@RestController
@RequestMapping("/auth") // 我们把路径改成/auth,更符合语义
public class AuthController { // 类名也改一下

    @Autowired
    private UserService userService;

    // 注册
    @PostMapping("/register")
    public User register(@RequestBody User user) {
        return userService.register(user);
    }

    // 登录
    @PostMapping("/login")
    public Map<String, String> login(@RequestBody Map<String, String> credentials) {
        String username = credentials.get("username");
        String password = credentials.get("password");
        String token = userService.login(username, password);
        if (token != null) {
            return Map.of("token", token);
        }
        // 在实际项目中,应该返回更详细的错误信息
        throw new RuntimeException("Invalid credentials");
    }
}

我们把原来的 UserController 改成了 AuthController,专门负责认证。原来的查询用户接口暂时可以注释掉,因为它们还没有受到保护。

第五步:配置Spring Security

这是最硬核的一步,我们要告诉Spring Security这个“老头”按我们的规矩来办事:放行登录和注册接口,其他的接口需要带“令牌”才能访问。

修改我们刚才创建的 SecurityConfig.java

// ... imports ...
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // ... BCryptPasswordEncoder的Bean ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable() // 关闭csrf
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll() // 放行/auth/下的所有请求
            .anyRequest().authenticated() // 其他所有请求都需要认证
            .and()
            // 我们不再需要session了,所以把它关了
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
            
        // 这里我们后面还要加上JWT的过滤器
    }
}

注意WebSecurityConfigurerAdapter 在新版的Spring Security中已被弃用,但为了方便理解,我们暂时先用着。新版的配置方式我们后续可以再升级。

现在,重启你的应用。你会发现,如果你直接访问 http://localhost:8080/users(假如你没删掉这个接口),会直接返回401或403错误,说明我们的“防盗门”已经生效了!

而你访问 /auth/register/auth/login 则是正常的。你可以试试注册一个新用户,然后用新用户登录,看看是否能成功拿到一长串的JWT Token。

下一期预告:
我们已经成功生成了Token,但还没用上它!服务器还不知道怎么去校验客户端传来的Token。下一章,我们将编写一个JWT认证过滤器,把它加入到Spring Security的“安保流程”中,真正完成认证闭环。同时,我们也要开始捣鼓一下“尘封已久”的前端代码,让它跟我们的新后端“对上暗号”!

源码下载地址:
https://download.youkuaiyun.com/download/THMAIL/91753658

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

THMAIL

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值