SpringSecurity6+JWT实现前后端分离

本文使用mybatis-plus作为ORM框架,然后使用springsecurity6作为安全框架,采用JWT作为权限确认的方式。

其实我现在就刚入门这个springsecurity6而已,有许多都没有摸清它的使用,写下这篇文章只是想记录一下,下次想用的时候来看看。

一. 准备阶段

1.JWT工具类

上面说了使用JWT。那就得有一个能生成JWT和检测JWT的工具类,我在下面给出了一个简陋的工具类,包含了三个方法。这个工具类的jar包依赖如下:

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

本文的JWT工具类的加密算法就普普通通的HS256,token有效期就只有4个小时,这个token里面就只存有userName这一个信息,所以为了确保不会出错,在数据库中的userName这一列,不能出现一模一样的两行数据,换句话说,就是用户名不能重复。代码如下: 

@Component
@Slf4j
public class JwtUtil {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 4; //token过期时间,4个小时
    public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥

    //生成token字符串的方法
    public String getToken(String userName){
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setHeaderParam("alg", "HS256")
                .setSubject("user")
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
                .claim("userName", userName)//设置token主体部分 ,存储用户信息
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
    }

    //验证token字符串是否是有效的  包括验证是否过期
    public boolean checkToken(String jwtToken) {
        if(jwtToken == null || jwtToken.isEmpty()){
            log.error("Jwt is empty");
            return false;
        }
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
            Claims body = claims.getBody();
            if ( body.getExpiration().after(new Date(System.currentTimeMillis()))){
                return true;
            } else
                return false;
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
    }

    public Claims getTokenBody(String jwtToken){
        if(jwtToken == null || jwtToken.isEmpty()){
            log.error("Jwt is empty");
            return null;
        }
        try {
            return Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken).getBody();
        } catch (Exception e){
            log.error(e.getMessage());
            return null;
        }
    }
}

2.数据库连接

本项目使用mybatis-plus作为ORM框架,然后使用druid连接池,所需要的依赖如下:

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.15</version>
        </dependency>

接着在application.yml中进行配置,我的数据库名字起名叫dubbd,你们可以根据自己数据库名字将dubbd更换。

server:
  port: 8080

datasource:
  url: localhost:3306/dubbd

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://${datasource.url}?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false&maxReconnects=10&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    hikari:
      maximum-pool-size: 10
      max-lifetime: 1770000
    druid:
      validation-query: SELECT 1 FROM DUAL
      initial-size: 10
      min-idle: 10
      max-active: 200
      min-evictable-idle-time-millis: 300000
      test-on-borrow: false
      test-while-idle: true
      time-between-eviction-runs-millis: 30000
      pool-prepared-statements: true
      max-open-prepared-statements: 100

再然后,我们要在数据库的一个表中准备自己需要的用户数据,我的用户数据如下:

导出后的sql脚本如下:

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `userName` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  `role` varchar(20) DEFAULT NULL,
  `userId` varchar(100) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户信息表';

LOCK TABLES `user` WRITE;

INSERT INTO `user` VALUES ('huonzy','$2a$10$kNrjJ3.1wCSmSnjs1JI.RO6RMrQc.oRiJ93T2sefyCOzXb7yy.Mmm','user','1');

UNLOCK TABLES;

3.实现表和实体类的绑定

在上面,我们已经明确了我们的表名为user,所以要使用mybatis-plus,我们就要创建一个User类实体类,并使用注解来绑定这个表,因为我们同时还要使用User这个实体类作为springsecurity的用户类,所以我们在创建这个实体类时,还要实现UserDetails这个接口,下面是我们需要的依赖:

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

接下来是实体类的代码: 

@TableName("user")
@Data
public class User implements UserDetails {

    @TableField("userName")
    private String userName;

    @TableField("password")
    private String password;

    @TableId("userId")
    private String userId;

    @TableField("role")
    private String role;

    @Override//用户所拥有的权限,返回的列表中至少得有一个值,否则这个用户啥权限都没有
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority(role));
    }

    @Override//实现UserDetails的getPassword方法,返回实体类的password
    public String getPassword() {
        return password;
    }

    @Override//这个方法是UserDetails中的方法,必须实现
    public String getUsername() {
        return userName;
    }

    public String getUserName(){//这个是mybatis-plus需要用到的方法
        return this.userName;
    }

    @Override//返回true,代表用户账号没过期
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override//返回true,代表用户账号没被锁定
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override//返回true,代表用户密码没有过期
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override//返回true,代表用户账号还能够使用
    public boolean isEnabled() {
        return true;
    }
}

 最后就是建立mapper层的CRUD了:

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

不要忘记在启动类下加上@MapperScan注解。

4.统一返回前后端交互的数据格式

前端登录时传给后端的,无疑就是用户名和密码了,我们创建一个类来接收者两个数据:

@Data
public class LoginReuqest {
    private String userName;
    private String password;
}

后端传给前端的,就是一个统一的格式:

@Data
@Builder
public class HnResult {
    private Integer code;
    private String msg;
    private Object data;

    public static HnResult ok(String message) {
        return HnResult.builder().code(200).msg(message).build();
    }

    public static HnResult ok() {
        return HnResult.builder().code(200).msg("成功").build();
    }

    public static HnResult error(String message){
        return HnResult.builder().code(500).msg(message).build();
    }

    public static HnResult error(){
        return HnResult.builder().code(500).msg("失败").build();
    }

    public <T> HnResult setData(T data){
        this.data = data;
        return this;
    }
}

到这里,准备都已经完成了,开始进入登录的用户信息确认阶段。

二.用户登录认证阶段

当一个用户访问一个网站时,那必须要进行登录的,没有账号的话那就去注册。用户登录只需要一次就够了,登录后,后面发出的请求就不需要再进行登录认证了。所以我们要确保这个过程只用经历一次。对于用户登录的信息确认,我们有两种实现方式,第一种就是实现UsernamePasswordAuthenticationFilter;第二种就是自定义一个控制层的用户登录接口,然后在这个接口里面使用AuthenticationManager来对用户信息进行确认。其实这两种方式差不多,UsernamePasswordAuthenticationFilter也会调用AuthenticationManager来对用户信息进行确认。第二种方式就是直接把UsernamePasswordAuthenticationFilter中的doFilterInternal方法里面的代码搬到了控制层的登录接口里面而已。所以我们在这里使用第二种方式,会了第二种方式,第一种方式也差不多会了。因为实现UsernamePasswordAuthenticationFilter,只需要实现doFilterInternal方法。

1.实现UserDetailsService

在前面的准备阶段,我们定义了一个实体类User并让其实现了UserDetails接口,之所以我们要实现这个接口,是因为springsecurity中的认证过程中采用的用户服务返回的数据格式就是UserDetails。这个用户服务就是UserDetailsService,同样的,这个也是一个接口,也需要创建一个服务类对其进行实现,我们只需要实现其loadUserByUsername方法就行了,代码如下:

@Service
@Slf4j
public class UserService implements UserDetailsService {

    @Resource
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName, username);
        try {
            User user = userMapper.selectOne(wrapper);//这里要确保userName是唯一的
            return user;
        }catch (Exception e){
            log.error("user is not find");
            return null;
        }
    }

}

接下来,就是对SpringSecurity进行修改配置了。

2.将PasswordEncoder注册成Bean

 我们的用户登录认证流程其实就是上面的这幅图,里面用到的类,除了UsernamePasswordAuthenticationToken不需要注册成Bean外,其余都要注册成Bean。我们先注册PasswordEncoder,这个其实是一个接口,我们需要将其实现类注册成接口。我们需要创建一个配置类,用来容纳所有的springsecurity配置修改,然后在里面将PasswordEncoder注册成Bean,代码如下:

@Configuration//声明该类是一个配置类
@EnableWebSecurity//开启springsecurity配置修改
public class SecurityConfig {


    @Bean//PasswordEncoder的实现类为BCryptPasswordEncoder
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

3.将AuthenticationProvider注册成Bean

我们从上图可以看到,AuthenticationProvider使用了PasswordEncoder和UserDetailsService,所以我们在配置 AuthenticationProvider时要将这两个加上。将下面的代码加入上面创建的SecurityConfig配置类即可。

    @Resource
    UserService userService;

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

4.将AuthenticationManager注册成Bean

将AuthenticationManager配置成Bean就十分简单了,也是将下面的代码加入SecurityConfig中:

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

5.将SecurityFilterChain注册成Bean

不知道大家有没有发现,我们上面并没有建立其AuthenticationManager和AuthenticationProvider的联系,那AuthenticationManager该如何使用AuthenticationProvider进行用户登录认证。AuthenticationManager在注册成Bean的时候,用的是配置里面的AuthenticationManager,所以我们需要在SecurityFilterChain中修改认证时使用的AuthenticationProvider变成我们自己注册的Bean,这样子AuthenticationManager就可以使用我们自己配置的AuthenticationProvider了。对SecurityFilterChain也需要写入SecurityConfig中,代码如下:

    @Resource
    JwtFilter jwtFilter;//后面jwt验证需要用到的过滤器,现在先不理它

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.formLogin(AbstractHttpConfigurer::disable)//取消默认登录页面的使用
                .logout(AbstractHttpConfigurer::disable)//取消默认登出页面的使用
                .authenticationProvider(authenticationProvider())//将自己配置的PasswordEncoder放入SecurityFilterChain中
                .csrf(AbstractHttpConfigurer::disable)//禁用csrf保护,前后端分离不需要
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//禁用session,因为我们已经使用了JWT
                .httpBasic(AbstractHttpConfigurer::disable)//禁用httpBasic,因为我们传输数据用的是post,而且请求体是JSON
                .authorizeHttpRequests(request -> request.requestMatchers(HttpMethod.POST, "/user/login", "/user/register").permitAll().anyRequest().authenticated())//开放两个接口,一个注册,一个登录,其余均要身份认证
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);//将用户授权时用到的JWT校验过滤器添加进SecurityFilterChain中,并放在UsernamePasswordAuthenticationFilter的前面
        return httpSecurity.build();
    }

在上面的代码中,我禁用了默认登录和登出页面,因为我们是前后端分离,不需要后端提供页面。我禁用了session的创建和使用,因为我已经使用了JWT,即java web token,我禁用了CSRF保护,因为前后端分离不需要这种保护。

6.实现控制层登录接口

登录接口的实现其实可以按照上面那幅图的流程来写,不过在用户验证成功后要把信息存入SecurityContext中,想要获取SecurityContext就要通过SecurityContextHolder的getContext方法获取。

@RestController
@RequestMapping("/user/")
@Slf4j
public class UserController {

    @Resource
    AuthenticationManager authenticationManager;

    @PostMapping("login")
    public HnResult doLogin(@RequestBody LoginReuqest request){
        try{
            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(request.getUserName(), request.getPassword());
            Authentication authentication = authenticationManager.authenticate(auth);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserDetails userDetails = (UserDetails) authentication.getPrincipal();
            String jwtToken = jwtUtil.getToken(userDetails.getUsername());
            return HnResult.ok("登录成功").setData(jwtToken);
        } catch (Exception e){
            log.error(e.getMessage());
            log.error("userName or password is not correct");
            return HnResult.error("登录失败");
        }
    }

}

7.接口测试

使用apifox进行测试这个登录接口能不能成功返回数据。

三.用户注册阶段

用户注册也要构建一个控制层的接口,注意要把用户的密码经过PasswordEncoder.encode()编码后,再存入数据库。

1.在UserService中再添加一个插入用户的方法

public User insertUser(User user){
        try {
            LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(User::getUserName, user.getUserName());
            if (userMapper.selectList(wrapper).size() == 0){
                userMapper.insert(user);
                return user;
            }else {
                log.error("userName already exists");
                return null;
            }
        }catch (Exception e){
            log.error(e.getMessage());
            return null;
        }
    }

2.构建注册接口

注册接口里面,我们用到了一个ID生成方法,该方法需要下面这个依赖:

<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-extra -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-extra</artifactId>
            <version>5.8.21</version>
        </dependency>

接口代码如下:

    @Resource
    UserService userService;

    @Resource
    PasswordEncoder passwordEncoder;

    @PostMapping("register")
    public HnResult doRegister(@RequestBody User user){
        try {
            if (user.getPassword() != null && !user.getPassword().isEmpty()){
                String password = passwordEncoder.encode(user.getPassword());
                user.setPassword(password);
                user.setUserId(IdUtil.getSnowflakeNextIdStr());
                if (userService.insertUser(user) == null){
                    throw new Exception("用户名已存在");
                }
                String jwtToken = jwtUtil.getToken(user.getUserName());
                return HnResult.ok().setData(jwtToken);
            }else
                throw new Exception("密码为空");
        }catch (Exception e){
            log.error(e.getMessage());
            return HnResult.error("注册失败" + e.getMessage());
        }
    }

3.接口测试

数据库里的确多了一条数据。

 

四.用户权限验证阶段

用户在每发一个请求到后端这里,都要进行验证其权限是否足够访问这个接口,在我这里的话,只需要验证jwt是否可用就行了。

1.构建JWT验证过滤器

代码如下:

@Component
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    @Resource
    JwtUtil jwtUtil;

    @Resource
    UserService userService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwtToken = request.getHeader("token");//从请求头中获取token
        if (jwtToken != null && !jwtToken.isEmpty() && jwtUtil.checkToken(jwtToken)){
            try {//token可用
                Claims claims = jwtUtil.getTokenBody(jwtToken);
                String userName = (String) claims.get("userName");
                UserDetails user = userService.loadUserByUsername(userName);
                if (user != null){
                    UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
                    SecurityContextHolder.getContext().setAuthentication(auth);
                }
            } catch (Exception e){
                log.error(e.getMessage());
            }
        }else {
            log.warn("token is null or empty or out of time, probably user is not log in !");
        }
        filterChain.doFilter(request, response);//继续过滤
    }
}

2.将JWT过滤器添加进SecurityFilterChain

这一步骤我们在配置 SecurityFilterChain的时候已经做过了,大家可以返回到第二部分那里去看。

3.验证和测试

将之前注册好的用户的token放入请求头中,参数名为token,参数值则为之前返回的jwt。随便写一个接口,然后访问这个接口。

    @PostMapping("check")
    public HnResult doCheck(){
        log.info("权限验证成功");
        return HnResult.ok();
    }

 

Spring Boot和Vue.js是两个非常流行的技术栈,可以非常好地实现前后端分离的开发模式。SecurityJWT是两个很好的工具,可以帮助我们实现安全的登录和授权机制。 以下是实现Spring Boot和Vue.js前后端分离的步骤: 1.创建Spring Boot工程 首先,我们需要创建一个Spring Boot工程,可以使用Spring Initializr来生成一个基本的Maven项目,添加所需的依赖项,包括Spring SecurityJWT。 2.配置Spring SecuritySpring Security中,我们需要定义一个安全配置类,该类将定义我们的安全策略和JWT的配置。在这里,我们可以使用注解来定义我们的安全策略,如@PreAuthorize和@Secured。 3.实现JWT JWT是一种基于令牌的身份验证机制,它使用JSON Web Token来传递安全信息。在我们的应用程序中,我们需要实现JWT的生成和验证机制,以便我们可以安全地登录和授权。 4.配置Vue.js 在Vue.js中,我们需要创建一个Vue.js项目,并使用Vue CLI来安装和配置我们的项目。我们需要使用Vue Router来定义我们的路由,并使用Axios来发送HTTP请求。 5.实现登录和授权 最后,我们需要实现登录和授权机制,以便用户可以安全地登录和访问我们的应用程序。在Vue.js中,我们可以使用Vue Router和Axios来发送HTTP请求,并在Spring Boot中使用JWT来验证用户身份。 总结 以上是实现Spring Boot和Vue.js前后端分离的步骤,我们可以使用SecurityJWT实现安全的登录和授权机制。这种开发模式可以让我们更好地实现前后端分离,提高我们的开发效率和应用程序的安全性。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Huonzy

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

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

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

打赏作者

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

抵扣说明:

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

余额充值