Kết hợp Vue3 và Spring Security với JWT để thực hiện xác thực đăng nhập

Mục lục

Frontend:

Database:

Backend:

Chạy ứng dụng:

Trong thời gian gần đây, tôi đã viết một bài hướng dẫn chi tiết về Spring Security, nhưng chưa áp dụng vào thực tế.

Vì vậy, lần này sẽ trình bày cách sử dụng Spring Security để thực hiện xác thực đăng nhập trong một dự án thực tế. Trong ví dụ này, chúng ta sẽ sử dụng mô hình phát triển tách biệt frontend và backend. Frontend được xây dựng bằng Vue3, sử dụng thư viện thành phần Element Plus,封装 axios, quản lý trạng thái Pinia và định tuyến Router. Backend sử dụng Spring Boot tích hợp Spring Security và JWT để thực hiện xác thực đăng nhập.

Bài viết này phù hợp với những người đã có kiến thức nền tảng. Nếu bạn chưa hiểu rõ về framework bảo mật Spring Security, hãy xem bài viết trước đó của tôi về Spring Security:

SpringBoot3 tích hợp SpringSecurity để thực hiện xác thực đăng nhập và phân quyền (Giải thích chi tiết 10.000 từ) - CSDN Blog

Phiên bản công nghệ sử dụng: Vue3.3.11, SpringBoot3.1.5, Spring Security6.x

Quy trình nghiệp vụ:

Quy trình này khá đơn giản, tiếp theo chúng ta sẽ bắt đầu triển khai mã nguồn dựa trên quy trình này.

Frontend:

Tạo một dự án Vue mới và thêm các thư viện cần thiết; Dự án này sử dụng: Element Plus, Axios, quản lý trạng thái Pinia, định tuyến Router (Lưu ý rằng trong dự án, Pinia cần được cài đặt plugin lưu trữ vĩnh viễn)

Sử dụng Pinia trong Vue3 và lưu trữ vĩnh viễn (Giải thích chi tiết) - CSDN Blog

Tạo hai thành phần trong dự án Vue: Login.vue (thành phần đăng nhập, chịu trách nhiệm hiển thị trang đăng nhập) và Layout.vue (trang bố cục, chịu trách nhiệm bố cục toàn bộ dự án, sau khi đăng nhập thành công sẽ chuyển hướng đến trang này)

Định nghĩa tuyến đường: Tạo file index.ts trong thư mục router

import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/',
      name: 'login',
      component: () => import('@/components/Login.vue')
    },
    {
      path: '/layout',
      name: 'layout',
      component: () => import('@/components/Layout.vue')
    }
  ]
})

export default router

Định nghĩa thành phần Login là mặc định và thành phần Layout;

Bao đóng trạng thái Token: Tạo file useToken.ts trong thư mục stores

import { defineStore } from 'pinia'
import { ref } from 'vue'
const useTokenStore = defineStore('token', ()=>{
const token=ref()

const removeToken=()=>{
    token.value=''
}

return {token,removeToken}
},
{persist: true}
)

export default useTokenStore

Bao đóng Axios: Tạo file request.ts trong thư mục utils

import axios from "axios";
import  useTokenStore from '@/stores/useToken'
import { ElMessage } from 'element-plus';
// Khởi tạo một api
const api = axios.create({
    baseURL: "http://localhost:8888",
    timeout: 5000
});
// Giao diện trước khi gửi yêu cầu
api.interceptors.request.use(
    config =>{
const useToken = useTokenStore();
if(useToken.token){
    console.log("Yêu cầu header token=====>", useToken.token);
    // Thiết lập header yêu cầu
    config.headers.token = useToken.token;
}
        return config;

},
error =>{

    return Promise.reject(error);
}
)

// Giao diện trước khi nhận phản hồi
api.interceptors.response.use(
    response =>{
        console.log("Dữ liệu phản hồi", response);
if(response.data.code !=200){
    ElMessage.error(response.data.message);
}

        return response;
},
error =>{
    return Promise.reject(error);
}
)

export default api;

Trước khi gửi yêu cầu, chúng ta sẽ thêm token vào header. Trong file request.ts, chúng ta import useToken và kiểm tra nếu token không rỗng thì thêm token vào header.

Trước khi nhận phản hồi, chúng ta cũng thực hiện chặn lại, nếu mã trạng thái trả về từ backend không phải 200 thì in ra thông báo lỗi;

Tiếp theo, chúng ta có thể bắt đầu triển khai logic đăng nhập trong Login.vue (Tôi sẽ trực tiếp sao chép nội dung thành phần, không phức tạp lắm, chủ yếu là biểu mẫu của Element Plus):

<template>
    <div class="background" style="font-family:kaiti" >

<!-- Biểu mẫu đăng ký -->
<el-dialog v-model="isRegister" title="Đăng ký người dùng" width="30%">
    <el-form label-width="120px" v-model="registerForm">
        <el-form-item label="Tên người dùng">
            <el-input type="text"   v-model="registerForm.username"   >
              <template #prefix>
                <el-icon><Avatar /></el-icon>
              </template>
            </el-input>
        </el-form-item>
        <el-form-item label="Mật khẩu">
            <el-input  type="password" v-model="registerForm.password" >
              <template #prefix>
        <el-icon><Lock /></el-icon>
        </template>
            </el-input>
        </el-form-item>
        <el-form-item>
            <el-button type="primary" @click="registerAdd" >Gửi</el-button>
            <el-button @click="isRegister = false">Hủy</el-button>
        </el-form-item>
    </el-form>
</el-dialog>

<!-- Hộp đăng nhập -->
<div class="login-box">
<el-form
    label-width="100px"
    :model="loginFrom"
    style="max-width: 460px"
    :rules="Loginrules"
    ref="ruleFormRef"
  >
    <el-form-item label="Tên người dùng"  prop="username">
      <el-input v-model="loginFrom.username"  clearable  >
        <template #prefix>
                <el-icon><Avatar /></el-icon>
              </template>
        </el-input>
    </el-form-item>
    <el-form-item label="Mật khẩu" prop="password">

      <el-input v-model="loginFrom.password"   show-password   clearable  type="password" >
        <template #prefix>
        <el-icon><Lock /></el-icon>
        </template>
      </el-input>
    </el-form-item>

    <el-form-item label="Mã xác minh"  prop="codeValue">
      <el-input v-model="loginFrom.codeValue"  style="width: 100px;"  clearable  >
      </el-input>
      <img :src="codeImage" @click="getCode" style="transform: scale(0.9);"/>
    </el-form-item>

    <el-button type="success" @click="getLogin(ruleFormRef)"  style="transform: translateX(50px)"  class="my-button">Đăng nhập</el-button>
    <el-button type="primary" @click="isRegister=true" class="my-button">Đăng ký</el-button>
  </el-form>

</div>

    </div>
</template>

<script lang="ts" setup>
import { ref,onMounted,reactive } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage } from 'element-plus';
import useTokenStore  from '@/stores/useToken'
import  api  from '@/utils/request'
import type { FormInstance, FormRules } from 'element-plus'
const ruleFormRef =  ref<FormInstance>()

const loginFrom=ref({
username:'',
password:'',
codeKey:'',
codeValue:''
})

const Loginrules=reactive({

  username: [
    { required: true, message: 'Vui lòng nhập tên người dùng', trigger: 'blur' }
  ],
  password: [
    { required: true, message: 'Vui lòng nhập mật khẩu', trigger: 'blur' },
    { min: 6, max: 12, message: 'Độ dài từ 6 đến 12 ký tự', trigger: 'blur'}
  ],
  codeValue: [
    { required: true, message: 'Vui lòng nhập mã xác minh', trigger: 'blur' }
  ]

})

const registerForm=ref({
  username:'',
  password:''
})

const codeImage=ref('')

const isRegister=ref(false)

const tokenStore = useTokenStore();

const router = useRouter()

const getLogin = async(formEl: FormInstance | undefined) => {

  if (!formEl)  return

  await formEl.validate((valid, fields) => {
    if (valid) {
      console.log('Gửi!')
      
    } else {
      ElMessage('Vui lòng nhập đầy đủ thông tin')
      return;
    }
  })

  let {data}=await api.post('/user/login',loginFrom.value)

if(data.code==200){
  ElMessage('Đăng nhập thành công')
  console.log(data);
  tokenStore.token=data.data
  router.replace({name:'layout'})
}else{

  ElMessage('Đăng nhập thất bại')
}


}

const getCode=async()=>{
  let {data}=await api.get('/getCaptcha')
  loginFrom.value.codeKey=data.data.codeKey
  codeImage.value=data.data.codeValue

}

const registerAdd=async()=>{
let {data}=await api.post('/user/register',registerForm.value)

if(data.code==200){
  ElMessage('Đăng ký thành công')
  isRegister.value=false
}else{

  ElMessage('Đăng ký thất bại')
  isRegister.value=false
  }

}

// Lấy mã xác minh khi tải trang xong

onMounted(()=>{
getCode()

})

</script>

Trang này còn bao gồm mã xác minh hình ảnh và biểu mẫu đăng ký. Những phần khác tương tự như đăng nhập thông thường;

Kết quả cuối cùng của trang này như hình:

Trong trang Layout.vue, chúng ta chỉ thực hiện hai phương thức kiểm tra; một là lấy thông tin người dùng cụ thể, hai là nút thoát đăng nhập;

<template>
    <div class="common-layout">
    <el-container>
          <el-header height="100px">
            Phần đầu
            <el-button type="primary" @click="getUserInfo">Lấy thông tin người dùng</el-button>
            <el-button type="success" @click="Logout">Thoát đăng nhập</el-button>
        </el-header>
    
    <el-container>
    <el-aside width="200px">
        Thanh menu
    </el-aside>
    <el-main>
        Khu vực hiển thị
    </el-main>
    </el-container>
    </el-container>
  </div>


</template>

<script lang="ts" setup name="Layout">
import { ref } from 'vue'
import api from '@/utils/request'
import {ElMessage} from 'element-plus'
import { useRouter } from 'vue-router'
import  useToeknStore from '@/stores/useToken'
const router = useRouter()

const Logout =async () => {
 let data= api.get("/user/logout")
if(data.data.code==200){
ElMessage.success('Thoát thành công')
// Xóa token
useToeknStore().removeToken
router.replace({name:'login'})
}
else{
  ElMessage.error('Thoát thất bại')
}

}

const getUserInfo = async() => {

let data=await api.get("/user/info")

console.log('@',data);

}


</script>

Database:

Tạo một bảng dữ liệu để xác thực đăng nhập:

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
 status INT DEFAULT 0
);

Bảng này chỉ có các trường đơn giản như tên người dùng, mật khẩu và trạng thái người dùng;

Backend:


Tạo một dự án Spring Boot mới và thêm các phụ thuộc sau:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>4.3.0</version>
</dependency>

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.18</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.21</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Backend sử dụng MybatisPlus cho các thao tác thêm, sửa, xóa, tìm kiếm người dùng. Controller, service, mapper cơ bản sẽ không được trình bày ở đây;

Tạo lớp MyTUserDetail kế thừa UserDetails:

@Data
public class MyTUserDetail implements Serializable, UserDetails {

    private static final long serialVersionUID = 1L;

    private Users Users;

    @JsonIgnore  // Bỏ qua json
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        return null;
    }
    
    @JsonIgnore
    @Override
    public String getPassword() {
        return this.getUsers().getPassword();
    }
    @JsonIgnore
    @Override
    public String getUsername() {
        return this.getUsers().getUsername();
    }

    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return this.getUsers().getStatus()==0;
    }
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return this.getUsers().getStatus()==0;
    }
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return this.getUsers().getStatus()==0;
    }
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return this.getUsers().getStatus()==0;
    }
}

Tạo lớp MyUserDetailServerImpl, thực hiện phương thức loadUserByUsername của interface MyUserDetailServer:

@Service
public class MyUserDetailServerImpl implements MyUserDetailServer {
    @Autowired
    UserMapper userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.selectOne(new LambdaQueryWrapper<User>().
                eq(username != null, User::getUsername, username));
        if (tUser == null) {
            throw new UsernameNotFoundException("Tên người dùng không tồn tại");
        }

        MyTUserDetail myTUserDetail=new MyTUserDetail();
myTUserDetail.setUser(user);
        return myTUserDetail;
    }
}

Tạo lớp công cụ JwtUtils để tạo token;

@Component
public class JwtUtil {

    private final String secret="zhangqiao";

    private final Long expiration=36000000L;

    public String generateToken(Integer id) {
        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + expiration);

        Algorithm algorithm = Algorithm.HMAC256(secret);
        return JWT.create()
                .withSubject(String.valueOf(id))
                .withIssuedAt(now)
                .withExpiresAt(expiryDate)
                .sign(algorithm);
    }

    public Integer getUsernameFromToken(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return Integer.valueOf(jwt.getSubject());
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /*
    * Kiểm tra xem token có hết hạn không
    * */
    public boolean isTokenValid(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWT.require(algorithm).build().verify(token);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    /*
    * Làm mới token
    * */

    public String refreshToken(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            String username = jwt.getSubject();
            Algorithm algorithm = Algorithm.HMAC256(secret);

            Date now = new Date();
            Date expiryDate = new Date(now.getTime() + expiration);

            return JWT.create()
                    .withSubject(username)
                    .withIssuedAt(now)
                    .withExpiresAt(expiryDate)
                    .sign(algorithm);
        } catch (JWTDecodeException e) {
            return null;
        }
    }
}

Tạo lớp lọc Jwt, kế thừa OncePerRequestFilter, dùng để chặn yêu cầu trước mỗi lần gọi và lấy token, sau đó kiểm tra xem token có đúng với dữ liệu người dùng trong bảng hay không;

Nếu đúng, lưu thông tin người dùng vào Security, các bộ lọc sau có thể lấy được thông tin người dùng. Nếu không đúng, cho phép tiếp tục. Chúng ta sẽ thêm bộ lọc này vào chuỗi bộ lọc trước UsernamePasswordAuthenticationFilter.

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // Lấy token từ header yêu cầu
        String token = request.getHeader("token");
        System.out.println("Thông tin token từ frontend=======>"+token);
        // Nếu token rỗng, cho phép tiếp tục
        if(!StringUtils.hasText(token)){
            filterChain.doFilter(request,response);
            return;
        }

//        Phân tích ID người dùng từ JWT
        Integer userId = jwtUtil.getUsernameFromToken(token);
        // Lấy thông tin người dùng từ redis
        String redisUser = redisTemplate.opsForValue().get(String.valueOf(userId));
        if(!StringUtils.hasText(redisUser)){
            filterChain.doFilter(request,response);
            return;
        }

        MyTUserDetail myTUserDetail= JSON.parseObject(redisUser, MyTUserDetail.class);

        // Lưu thông tin người dùng vào SecurityContextHolder, các bộ lọc sau có thể lấy được thông tin người dùng. Điều này cho thấy người dùng đã đăng nhập, các bộ lọc sau sẽ không cần chặn nữa
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,null);
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        filterChain.doFilter(request,response);
    }
}

Cấu hình Security:

(Vì chúng ta sử dụng mô hình tách biệt frontend và backend, nên không cần sử dụng phương thức formLogin mặc định của Spring Security)

Phương thức formLogin là cách cấu hình xác thực đăng nhập dựa trên biểu mẫu trong Spring Security. Nó thường được sử dụng trong các ứng dụng web truyền thống, nơi frontend được tạo động bởi backend và người dùng nhập tên người dùng và mật khẩu để đăng nhập. Trong trường hợp này, Spring Security xử lý yêu cầu đăng nhập, xác thực danh tính người dùng, tạo phiên làm việc, v.v.

Trong mô hình phát triển tách biệt frontend và backend, frontend và backend hoàn toàn tách biệt, frontend chịu trách nhiệm render giao diện và xử lý tương tác người dùng, backend cung cấp API và dịch vụ dữ liệu. Vì vậy, thường không sử dụng phương thức formLogin vì frontend không đăng nhập thông qua trang được render bởi backend. Backend chỉ cần trả về dữ liệu và trạng thái tương ứng, việc chuyển hướng và render trang được thực hiện bởi frontend (Vue3).

@Configuration
@EnableWebSecurity
public class MyServiceConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

 

    /*
    * Chuỗi bộ lọc security
    * */
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
http.csrf(AbstractHttpConfigurer::disable);

http.authorizeHttpRequests((auth) ->
    auth
            .requestMatchers("/getCaptcha","user/login","user/register").permitAll()
            .anyRequest().authenticated()
);
http.cors(cors->{
    cors.configurationSource(corsConfigurationSource());
        });
// Thêm bộ lọc tùy chỉnh trước UsernamePasswordAuthenticationFilter
    http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);





    return http.build();
}

@Autowired
private MyUserDetailServerImpl myUserDetailsService;

/*
* Quản lý xác thực
* */
    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){
        DaoAuthenticationProvider provider=new DaoAuthenticationProvider();
// Nhập UserDetailsService đã viết
        provider.setUserDetailsService(myUserDetailsService);
// Thêm bộ mã hóa mật khẩu
        provider.setPasswordEncoder(passwordEncoder);
// Đặt provider vào AuthenticationManager
        ProviderManager providerManager=new ProviderManager(provider);
        return providerManager;
    }

// Cấu hình CORS
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("*"));
        configuration.setAllowedMethods(Arrays.asList("*"));
        configuration.setAllowedHeaders(Arrays.asList("*"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    /*
    * Bộ mã hóa mật khẩu*/
@Bean
    public PasswordEncoder passwordEncoder(){
    return new BCryptPasswordEncoder();
}
}

Trong lớp cấu hình Security, chúng ta thiết lập vấn đề CORS, cấu hình chuỗi bộ lọc (cho phép một số endpoint nhất định, thêm bộ lọc JWT tùy chỉnh vào chuỗi bộ lọc của security), bộ mã hóa mật khẩu, quản lý xác thực, v.v.;

Controller UserController:

@RestController
@RequestMapping("/user")
public class UsersController {

    @Autowired
    private IUsersService userService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    
private  RedisTemplate<String,String> redisTemplate;

@Autowired

private  JwtUtils jwtUtils;

    @PostMapping("/login")
    public Result<String> login(@RequestBody DtoLogin dtoLogin) {
        System.out.println(dtoLogin);
        String token = userService.login(dtoLogin);
        return Result.successData(token);
    }

    @PostMapping("/register")
    public Result register(@RequestBody DtoLogin dtoLogin) {
        System.out.println(dtoLogin);
        Users users = new Users();
        users.setUsername(dtoLogin.getUsername());
        users.setPassword(passwordEncoder.encode(dtoLogin.getPassword()));
        userService.save(users);
        return Result.success();

    }
    @Autowired
    private RedisTemplate<String,String> redisTemplate;

    @Autowired
    private JwtUtil jwtUtil;

  

    @GetMapping("/info")
    public Result info(@RequestHeader("token")String token){
        System.out.println("Token nhận được từ controller=======>"+token);
        Integer id = jwtUtil.getUsernameFromToken(token);
        String redisUser = redisTemplate.opsForValue().get(String.valueOf(id));
        MyTUserDetail myTUserDetail = JSON.parseObject(redisUser, MyTUserDetail.class);
        return Result.successData(myTUserDetail);

    }

@GetMapping("user/logout")
 public Result logout(@RequestHeader("token")String token){

//        Phân tích ID người dùng từ JWT
        Integer userId = jwtUtil.getUsernameFromToken(token);

// Xóa ngữ cảnh SpringSecurity
  SecurityContextHolder.clearContext();

// Xóa dữ liệu người dùng trong redis
redisTemplate.delete(Integer.toString(userId));

return Result.success();
}
}

Trong controller UserController, do phương thức đăng nhập phức tạp, tôi đã viết lại phương thức đăng nhập trong service, các phương thức còn lại như lấy thông tin người dùng, đăng ký người dùng và đăng xuất đều được thực hiện trực tiếp trong UserController;

Phương thức đăng nhập được viết lại trong service:

@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService {

    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public String login(DtoLogin dtoLogin) {
        String codeRedis = redisTemplate.opsForValue().get(dtoLogin.getCodeKey());
        if (!dtoLogin.getCodeValue().equals(codeRedis)){
            throw new ResultException(400,"Mã xác minh sai");
        }
        // Mã xác minh đúng, xóa mã xác minh trong redis
        redisTemplate.delete(dtoLogin.getCodeKey());

        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dtoLogin.getUsername(),dtoLogin.getPassword());

        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if(authenticate==null){
            throw new  ResultException(400,"Tên người dùng hoặc mật khẩu sai");
        }
//        Lấy thông tin người dùng trả về
        Object principal = authenticate.getPrincipal();

        MyTUserDetail myTUserDetail=(MyTUserDetail) principal;
        System.out.println(myTUserDetail);
//        Sử dụng JWT tạo token, truyền ID người dùng vào
        String token = jwtUtil.generateToken(myTUserDetail.getUsers().getId());
        redisTemplate.opsForValue().
                set(String.valueOf(myTUserDetail.getUsers().getId()), JSON.toJSONString(myTUserDetail),1, TimeUnit.DAYS);

        return token;
    }
}

Do chúng ta vẫn sử dụng mã xác minh, trong phương thức đăng nhập này, trước tiên kiểm tra mã xác minh, nếu đúng thì kiểm tra tên người dùng và mật khẩu truyền về. Nếu đúng, trả về token bằng JWT, token chứa ID người dùng;

Tới đây, toàn bộ mã nguồn frontend và backend đã hoàn tất. Hãy thử nghiệm cụ thể;

Chạy:

Vì bảng mới tạo chưa có dữ liệu, bây giờ tôi sẽ nhấn vào nút đăng ký trên frontend để thêm vài bản ghi người dùng;

Sau khi thêm dữ liệu, tôi sẽ dùng tài khoản vừa đăng ký để đăng nhập:

Sau khi đăng ký thành công, sẽ chuyển đến thành phần Layout.vue do người dùng tự định nghĩa:

Bây giờ, tôi nhấn vào nút "Lấy thông tin người dùng", vì đường dẫn này không được cho phép truy cập, khi truy cập sẽ bị bộ lọc JWT tùy chỉnh chặn, kiểm tra xem token trong header có đúng không. Nếu đúng sẽ cho phép tiếp tục. Nếu không đúng, sẽ chuyển đến bộ lọc xác thực đăng nhập.

Nhìn thấy trong console in ra thông tin người dùng. Điều này là đúng, vì yêu cầu lần này mang theo token đúng, nếu tôi thay đổi giá trị token trong header, có thể truy cập được vào giao diện lấy thông tin người dùng không?

Tôi đã thay đổi thông tin token trong header, ngay lập tức yêu cầu bị chặn và báo lỗi 403;

Bây giờ, tôi nhấn nút "Thoát đăng nhập", nó nên xóa giá trị token trong useToken và backend cũng sẽ xóa dữ liệu người dùng trong redis, đồng thời chuyển hướng đến trang đăng nhập. Backend cũng sẽ xóa dữ liệu người dùng trong redis;

Tất cả các nhiệm vụ đã hoàn thành.

Mã nguồn frontend và backend cụ thể được lưu trên Gitee, ai cần có thể tải xuống:

Vue-Security: Tách biệt frontend và backend với Security

Tôi sẽ tổng kết lại tư duy cụ thể:

Frontend gửi yêu cầu đến backend, nếu là yêu cầu đăng nhập, sẽ đi thẳng vào endpoint đăng nhập, tôi cho phép mọi người truy cập endpoint đăng nhập và thực thi logic đăng nhập. Nếu đăng nhập thành công, sẽ trả về một token, frontend và backend lưu token này vào useToken và mỗi lần yêu cầu sau sẽ mang theo token. Nếu đăng nhập thất bại, trả về thông báo lỗi.

Nếu frontend gửi yêu cầu không phải endpoint đăng nhập nhưng mang theo token đúng, nó sẽ bị bộ lọc JWT tùy chỉnh chặn, đọc thông tin người dùng từ đó và lưu vào security để sử dụng cho các bộ lọc sau. Nếu không mang theo token hoặc token không đúng, backend sẽ trả về mã trạng thái 403.

Tiếp theo: Xác thực quyền hạn Tách biệt frontend và backend, sử dụng Vue3 tích hợp SpringSecurity với JWT để thực hiện xác thực quyền hạn - CSDN Blog

Thẻ: Vue3 SpringBoot JWT Security authentication

Đăng vào ngày 13 tháng 6 lúc 06:06