侧边栏壁纸
博主头像
进一步,海阔天空 博主等级

进一步,海阔天空

  • 累计撰写 140 篇文章
  • 累计创建 19 个标签
  • 累计收到 7 条评论

目 录CONTENT

文章目录

使用Springboot+JPA+JWT创建一个登录系统[2023]

海阔天空
2023-06-27 / 0 评论 / 0 点赞 / 43 阅读 / 0 字

本文主要基于youtube上的这个视频 ,并做了简单优化,需要原视频的,可以跟我联系。完整的源代码我也放到了git仓库中,需要的自行下载。网上很多springboot整合JWT用户认证和授权的教程,但是不是springboot版本太低,就是代码不完整,而且很多都是通过编写自定义的filter或拦截器进行jwt拦截和解析,本文直接使用oauth2-resource-server类库,因而代码更简洁,今后升级也更简单,只要把springboot升级上去即可。

开发环境

原视频中是基于vscode进行开发的,需要安装以下两个插件,请自行安装。如果你是用intellij开发,可能跳过这个步骤。

总体框架

项目的总体框架如下,其中AuthenticationController是认证授权相关的,UserController和AdminController是两个简单的调用示例。

创建项目

这里我们可以通过start.spring.io创建项目。原视频中用的是maven项目,我这里用的是gradle项目,springboot也使用了最新的稳定版本3.1.1,其他基本一致。

创建好项目之后,我们把下载的zip压缩包解压,把所有文件复制到我们的vscode项目目录下。vscode打开目录后,点击JAVA PROJECTS栏,VSCode会自动识别项目并帮我们编译。如下图。

vscode-bako.gif

编译好之后,点击spring boot dashboard,在app一栏点debug即可启动项目。

vscode-mlgm.gif

上面是基于VSCode的开发环境配置,我个人还是比较喜欢用intellij进行开发(intellij的自动补全功能太好用了,特别是springboot,呵呵)。要用intellij进行开发,只要用intellij打开我们的这个项目即可。

配置数据库连接

打开我们的项目,把application.properties文件重命名为application.yml文件(我个人比较喜欢用yaml文件,因为yaml文件对中文的支持更好一些)。然后复制一份为application-dev.yml,打开application.yml文件,加入如下配置:

server:
  port: 8104
spring:
  profiles:
    active: dev

然后打开application-dev.yml文件配置数据库连接,开发环境下,我们用的是H2数据库,生产环境下会切换到postgres数据库。

spring:
  datasource:
    url: jdbc:h2:file:~/village-data-dev;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE
    driver-class-name: org.h2.Driver
    username: sa
    password: password
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    show-sql: true
    hibernate:
      ddl-auto: create-drop

此时启动项目,会出现找不到数据库目录的错误。创建一下"~/village-data-dev"目录然后重启项目即可(window下自行设置数据库目录)。

新建测试用的Controller

新建两个测试用的Controller,代码如下:

UserController代码:

package com.fitit100.village.controller;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/user")
@CrossOrigin("*")
public class UserController {
    @GetMapping("")
    public String helloUser() {
        return "User level access";
    }
}

AdminController代码:

package com.fitit100.village.controller;


import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/admin")
@CrossOrigin("*")
public class AdminController {
    @GetMapping("")
    public String helloAdmin() {
        return "Admin level access";
    }
}

重启项目,分别访问http://localhost:8104/api/v1/user和http://localhost:8104/api/v1/admin就可以看到返回的结果了。

整合spring security

再次打开start.spring.io,按下图步骤加入spring security依赖。

vscode-tvba.gif

加完security依赖后,重启项目,这时候访问前面的两个Controller,会要求输入用户名和密码,用户名为user,密码为控制台中打印出来的随机密码。这样spring security就整合进来了。

新建一个SecurityConfiguration类,加入如下配置:

package com.fitit100.village.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http.csrf(crsf -> crsf.disable())
                .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
                .build();
    }
}

重启项目,这时候我们又可以直接访问http://localhost:8104/api/v1/userhttp://localhost:8104/api/v1/admin了,因为这里我们设置所有请求都不需要认证。

建立数据库模型

既然是用户认证和授权,当然少不了用户和角色,下面开始建立数据库模型。

AppUser.java

package com.fitit100.village.models;

import jakarta.persistence.*;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "v_user")
@Data
public class AppUser implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "user_id")
    private Integer userId;
    @Column(unique = true)
    private String username;
    private String password;

    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_role_junction",
            joinColumns = {@JoinColumn(name = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "role_id")}
    )
    private Set<Role> authorities;

    public AppUser() {
        super();
        this.authorities = new HashSet<Role>();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

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

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

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

Role.java

package com.fitit100.village.models;

import jakarta.persistence.*;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

@Entity
@Table(name="v_role")
@Data
public class Role implements GrantedAuthority {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "role_id")
    private Integer roleId;
    @Column(unique = true)
    private String authority;
}

重启项目,将会自动创建v_user和v_role两个表。

自定义UserDetailsService

接下来我们要自定义UserDetailsService,实现从数据库里面读取用户信息进行用户认证。

新增一个类UserService.java,代码如下:

package com.fitit100.village.services;

import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.Role;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder encoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!username.equals("mlk")) {
            throw new UsernameNotFoundException("not mlk");
        }
        Set<Role> roles = new HashSet<>();
        roles.add(new Role(1, "USER"));

        return new AppUser(1, "mlk", encoder.encode("mlk"), roles);
    }
}

这里我们先创建了一个测试用的用户test,后面我们会改成从数据库里面读取用户。

修改SecurityConfiguration.java文件,内容如下:

package com.fitit100.village.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
    //新增
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //新增
    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService detailsService) {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(detailsService);
        return new ProviderManager(daoAuthenticationProvider);
    }

    //修改配置
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(crsf -> crsf.disable())
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());

        http.httpBasic();

        return http.build();
    }
}

重启项目,这时候我们就可以用test用户来访问我们的两个接口了。

实现从数据库中读取用户

OK,现在开始从数据库中读取用户信息。

新建UserRepository.java,代码如下:

package com.fitit100.village.repository;

import com.fitit100.village.models.AppUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<AppUser, Integer> {
    Optional<AppUser> findByUsername(String username);
}

新建RoleRepository.java,代码如下:

package com.fitit100.village.repository;

import com.fitit100.village.models.Role;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
    Optional<Role> findByAuthority(String authority);
}

修改VillageApiApplication.java,加入初始化用户的代码,这里添加了一个admin用户,密码为admin,角色为USER。

package com.fitit100.village;

import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.Role;
import com.fitit100.village.repository.RoleRepository;
import com.fitit100.village.repository.UserRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.HashSet;
import java.util.Set;

@SpringBootApplication
public class VillageApiApplication {

    public static void main(String[] args) {
        SpringApplication.run(VillageApiApplication.class, args);
    }
    //加入初始化用户的代码
    @Bean
    CommandLineRunner runner(
            RoleRepository roleRepository,
            UserRepository userRepository,
            PasswordEncoder passwordEncoder
    ) {
        return args -> {
            if (roleRepository.findByAuthority("ADMIN").isPresent()) return;
            Role adminRole = roleRepository.save(
                    new Role("ADMIN")
            );
            roleRepository.save(new Role("USER"));
            Set<Role> roles = new HashSet<>();
            roles.add(adminRole);
            AppUser admin = new AppUser(1, "admin", passwordEncoder.encode("admin"), roles);
            userRepository.save(admin);
        };
    }
}

修改UserService类,从数据库中读取用户信息。

package com.fitit100.village.services;

import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.Role;
import com.fitit100.village.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder encoder;

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /*if (!username.equals("mlk")) {
            throw new UsernameNotFoundException("not mlk");
        }
        Set<Role> roles = new HashSet<>();
        roles.add(new Role(1, "USER"));

        return new AppUser(1, "mlk", encoder.encode("mlk"), roles);*/
        //改成从数据库中读取用户信息
        return userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException("用户不存在!"));
    }
}

这时候重启项目,就可以通过用户admin登录了。

实现用户注册功能

上文中,我们的用户是在程序启动时通过初始化代码添加的,现在我们要添加用户注册功能,实现通过接口注册新用户。

添加一个AuthenticationService.java类,该类实现了用户注册逻辑,代码如下:

package com.fitit100.village.services;


import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.Role;
import com.fitit100.village.repository.RoleRepository;
import com.fitit100.village.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.Set;

@Service
@Transactional //启用事务
public class AuthenticationService {
    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public AppUser registerUser(String username, String password) {
        String encodePassword = passwordEncoder.encode(password);
        Role userRole = roleRepository.findByAuthority("USER").get();
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(userRole);

        AppUser user = new AppUser();
        user.setUsername(username);
        user.setPassword(encodePassword);
        user.setAuthorities(roleSet);
        return userRepository.save(user);
    }
}

新加一个RegistrationDTO.java类,代码如下:

package com.fitit100.village.models;

import lombok.Data;

@Data
public class RegistrationDTO {
    private String username;
    private String password;
}

新加一个AuthenticationController.java,代码如下:

package com.fitit100.village.controller;

import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.RegistrationDTO;
import com.fitit100.village.services.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/auth")
@CrossOrigin("*")
public class AuthenticationController {
    @Autowired
    private AuthenticationService authenticationService;

    @PostMapping("/register")
    public AppUser registerUser(@RequestBody RegistrationDTO body){
        return authenticationService.registerUser(body.getUsername(), body.getPassword());
    }
}

重启项目,用postman测试一下我们的用户注册接口,如下图:

这时候会提示没有认证,不用紧张,修改一下SecurityConfiguration.java中的代码即可,修改后的代码如下:

package com.fitit100.village.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService detailsService) {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(detailsService);
        return new ProviderManager(daoAuthenticationProvider);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(crsf -> crsf.disable())
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers("/api/v1/auth/**").permitAll(); //修改这里,放开/api/v1/auth/**的访问限制
                    auth.anyRequest().authenticated();
                });

        http.httpBasic(Customizer.withDefaults());

        return http.build();
    }
}

这时候再点击用户注册接口,就可以成功注册了。

启用JWT

前面事实上是一个典型的Spring Security项目配置过程,下面重点来了,启用JWT。

首选在build.gradle中添加oauth2-resource-server引用。

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.1'
	id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.fitit100'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' //添加oauth2-resource-server引用
}

tasks.named('test') {
	useJUnitPlatform()
}

添加KeyGeneratorUtility.java类,用于生成key

package com.fitit100.village.util;

import java.security.KeyPair;
import java.security.KeyPairGenerator;

public class KeyGeneratorUtility {
    public static KeyPair generateRsaKey() {
        KeyPair keyPair;
        try {
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
            keyPairGenerator.initialize(2048);
            keyPair = keyPairGenerator.generateKeyPair();
        } catch (Exception ex) {
            throw new IllegalStateException();
        }
        return keyPair;
    }
}

添加RSAKeyProperties.java类,用户获得私钥和密钥

package com.fitit100.village.util;

import lombok.Data;
import org.springframework.stereotype.Component;

import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;

@Component
@Data
public class RSAKeyProperties {
    private RSAPublicKey publicKey;
    private RSAPrivateKey privateKey;

    public RSAKeyProperties() {
        KeyPair keyPair = KeyGeneratorUtility.generateRsaKey();
        this.publicKey = (RSAPublicKey) keyPair.getPublic();
        this.privateKey = (RSAPrivateKey) keyPair.getPrivate();
    }
}

修改SecurityConfiguration.java的代码,加入jwtEncoder,jwtDecoder等,代码如下

package com.fitit100.village.config;

import com.fitit100.village.util.RSAKeyProperties;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
    private final RSAKeyProperties keys;

    public SecurityConfiguration(RSAKeyProperties keys) {
        this.keys = keys;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf(crsf -> crsf.disable())
                .authorizeHttpRequests(auth -> {
                    auth.requestMatchers("/api/v1/auth/**").permitAll();
                    auth.anyRequest().authenticated();
                })
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        //http.httpBasic(Customizer.withDefaults());

        return http.build();
    }

    @Bean
    public JwtDecoder jwtDecoder() {
        return NimbusJwtDecoder.withPublicKey(keys.getPublicKey()).build();
    }

    @Bean
    public JwtEncoder jwtEncoder() {
        JWK jwk = new RSAKey.Builder(keys.getPublicKey()).privateKey(keys.getPrivateKey()).build();
        JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
        return new NimbusJwtEncoder(jwks);
    }
}

类外,我们把代码优化一下,单独出一个ApplicationConfig.java,用于配置UserDetailsService,PasswordEncoder等Bean。代码如下:

package com.fitit100.village.config;


import com.fitit100.village.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
    private final UserRepository userRepository;

    @Bean
    public UserDetailsService userDetailsService() {
        return username -> userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("找不到该用户"));
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());
        return authProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

添加一个TokenService.java,用户生成JWT

package com.fitit100.village.services;

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class TokenService {
    private final JwtEncoder jwtEncoder;

    public String generateJwt(Authentication auth) {
        Instant now = Instant.now();
        String scope = auth.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(" "));
        JwtClaimsSet claimsSet = JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now)
                .subject(auth.getName())
                .claim("roles", scope)
                .expiresAt(Instant.ofEpochMilli(System.currentTimeMillis() + 1000 * 60 * 24))
                .build();
        return jwtEncoder.encode(JwtEncoderParameters.from(claimsSet)).getTokenValue();
    }
}

修改AuthenticationService.java,添加loginUser函数,用于用户登录认证并返回JWT。

package com.fitit100.village.services;


import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.LoginResponseDTO;
import com.fitit100.village.models.Role;
import com.fitit100.village.repository.RoleRepository;
import com.fitit100.village.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashSet;
import java.util.Set;

@Service
@RequiredArgsConstructor
@Transactional
public class AuthenticationService {
    private final UserRepository userRepository;

    private final RoleRepository roleRepository;

    private final PasswordEncoder passwordEncoder;

    private final AuthenticationManager authenticationManager;

    private final TokenService tokenService;

    public AppUser registerUser(String username, String password) {
        String encodePassword = passwordEncoder.encode(password);
        Role userRole = roleRepository.findByAuthority("USER").get();
        Set<Role> roleSet = new HashSet<>();
        roleSet.add(userRole);

        AppUser user = new AppUser();
        user.setUserId(0);
        user.setUsername(username);
        user.setPassword(encodePassword);
        user.setAuthorities(roleSet);
        return userRepository.save(user);
    }

    public LoginResponseDTO loginUser(String username, String password) {
        try {
            Authentication auth = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(username, password)
            );
            String token = tokenService.generateJwt(auth);
            return new LoginResponseDTO(userRepository.findByUsername(username).get(), token);
        } catch (AuthenticationException ex) {
            return new LoginResponseDTO(null, "");
        }
    }
}

添加LoginResponseDTO.java,用于登录后返回生成的jwt

package com.fitit100.village.models;

import lombok.Data;

@Data
public class LoginResponseDTO {
    private AppUser user;
    private String jwt;

    public LoginResponseDTO(AppUser user, String jwt) {
        this.user = user;
        this.jwt = jwt;
    }
}

然后在AuthenticationController.java中添加登录接口。

package com.fitit100.village.controller;

import com.fitit100.village.models.AppUser;
import com.fitit100.village.models.LoginResponseDTO;
import com.fitit100.village.models.RegistrationDTO;
import com.fitit100.village.services.AuthenticationService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/auth")
@CrossOrigin("*")
public class AuthenticationController {
    private final AuthenticationService authenticationService;

    public AuthenticationController(AuthenticationService authenticationService){
        this.authenticationService=authenticationService;
    }
    @PostMapping("/register")
    public AppUser registerUser(@RequestBody RegistrationDTO body) {
        return authenticationService.registerUser(body.getUsername(), body.getPassword());
    }

    @PostMapping("/login")
    public LoginResponseDTO loginUser(@RequestBody RegistrationDTO body) {
        return authenticationService.loginUser(body.getUsername(), body.getPassword());
    }
}

OK,现在重启项目,用postman测试登录接口

再访问接口http://localhost:8104/api/v1/admin,如果没有加入Token,将返回401错误,修改一下,把登录返回的jwt加入Header中,再点send按钮,就可以正常访问了,如下:

总结

到这里,我们的整个框架就建立起来了,后面只需要去简单的优化就可以了,例如我们注册成功后返回用户信息可以改掉,登录后也不应该返回用户信息,只需要返回jwt即可。使用jwt还有两个比较棘手的问题是刷新token和退出登录,这个我们后面有机会再去讲。本文对应的完整源代码可以在这里下载:https://github.com/kevinmeng0509/springboot_jwt_startup

0

评论区