SpringSecurity+jwt实现权限认证功能

系列文章目录

spring security+jwt安全方案


文章目录

前言

前面我们已经通过使用springboot框架获得了管理数据的基本能力,但是一个系统不和或缺的功能是安全登录。

这里我们以springsecurity+jwt方案实现登录以及权限控制。


一、springsecurity+jwt方案

提示:这里是对该方案的原理简介

一个安全的系统是需要对请求身份进行认证的。

但是http协议是无状态的,所以需要对每次的请求进行校验。

以下是jwt方案流程图,我们以现实生活为例。当我们被一个学校录取,我们在开学的时候需要提供身份证(类比账号密码),学校就会发放一个学生证(类比jwt令牌),这样我们每次进学校带学生证就行了(每次使用系统带jwt就行了)

二、权限控制RBAC

提示:这里是对登录的细化,即权限功能

登录系统的人并不只是一个人,以下为RBAC的数据库设计图。

我们依然以现实世界为例,一个教务系统,有很多用户(user);其中有两种身份(role):老师和学生;老师和学生拥有不一样的功能(menu),老师可以改卷子打分等等。

三、实现

我们以该图为例,该流程即为需要实现的。

1.RBAC数据库实现

这里请自行搜索RBAC的sql代码

2.拦截器实现

给系统套上一层拦截功能即是security实现的功能,这里先实现接口放行

在maven添加以下依赖后:

复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

系统会自动生成一个登录页面,我们需要做的就是给登录接口放行,其他接口拦截的配置

参考以下配置

复制代码
package com.nie.sportserver.config;

import com.nie.sportserver.Interceptor.JwtTokenAdminInterceptor;
import com.nie.sportserver.exception.MyAccessDeniedHandler;
import com.nie.sportserver.exception.MyAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;

import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.time.Duration;
import java.util.Arrays;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyAccessDeniedHandler myAccessDeniedHandler;
    @Autowired
    MyAuthenticationEntryPoint myAuthenticationEntryPoint;
    @Autowired
    JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    //加密算法
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //security配置跨域
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOriginPattern("*");
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");
        corsConfiguration.setAllowCredentials(true);
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    }

    //配置安全拦截
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()//关闭csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                //不通过Session获取Securitycontext
                .and()//配置异常处理
                .exceptionHandling()
                .authenticationEntryPoint(myAuthenticationEntryPoint)
                .accessDeniedHandler(myAccessDeniedHandler)
                .and()
                .authorizeRequests()
                //接口匿名访问
                .antMatchers("/doc.html",
                        "/favicon.ico",
                        "/v2/api-docs",
                        "/swagger-resources/**",
                        "/webjars/**","/user/login").anonymous()//携带token了就无法访问了
                .anyRequest().authenticated();
        http.addFilterBefore(jwtTokenAdminInterceptor, UsernamePasswordAuthenticationFilter.class);
    }

    //暴露认证方法变为bean对象
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

关键在于以下代码

复制代码
                .antMatchers("/doc.html",
                        "/favicon.ico",
                        "/v2/api-docs",
                        "/swagger-resources/**",
                        "/webjars/**","/user/login").anonymous()

3.登录接口实现

由于之前已经实现了放行,我们只需要完成查询数据库,并且将数据生成jwt即可

在上面的配置中,我们已经把认证方法暴露为bean对象,我们实现该方法即可

复制代码
    //暴露认证方法变为bean对象
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

示例如下

复制代码
package com.nie.sportserver.service.impl;


import com.nie.sportpojo.entity.LoginUser;

import com.nie.sportpojo.entity.User;
import com.nie.sportserver.mapper.LoginMapper;
import com.nie.sportserver.mapper.UserMapper;
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.stereotype.Service;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private LoginMapper loginMapper;
    @Autowired
    private UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询用户信息
        User user = loginMapper.getByUserName(username);

        //把数据封装为UserDetail返回
        //todo 查询对应的权限信息
        List<String> list = new ArrayList<>(userMapper.selectPermsByUserId(user.getId()));
        LoginUser loginUser = new LoginUser(user,list);
        return loginUser;
    }
}

4.拦截器实现

在前面的配置中,我们已经将普通请求拦截了,并且使用拦截器

复制代码
    @Autowired
    JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

这里来实现拦截器

复制代码
package com.nie.sportserver.Interceptor;



import com.nie.sportcommon.utills.JwtUtil;
import com.nie.sportpojo.entity.LoginUser;

import com.nie.sportpojo.entity.User;
import com.nie.sportserver.mapper.LoginMapper;
import com.nie.sportserver.mapper.UserMapper;
import com.nie.sportserver.properties.JwtProperties;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Component
@Slf4j
public class JwtTokenAdminInterceptor extends OncePerRequestFilter {
    @Autowired
    private JwtProperties jwtProperties;
    @Autowired
    private LoginMapper loginMapper;
    @Autowired
    private UserMapper userMapper;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        //从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());
        if (!StringUtils.hasText((token))) {
            filterChain.doFilter(request, response);
            return;
        }
        //校验令牌
        Long userId;
        try {
            log.info("jwt校验{}", token);
            //token解析
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            userId = Long.valueOf(claims.get("userId").toString());
            log.info("当前用户id:{}", userId);
        } catch (Exception ex) {
            throw new RuntimeException("token非法");
        }
        User user = loginMapper.getByUserId(userId);
        List<String> list = new ArrayList<>(userMapper.selectPermsByUserId(user.getId()));
        LoginUser loginUser = new LoginUser(user,list);
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

其他工具类

jwt依赖

复制代码
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

jwt工具类

复制代码
package com.nie.sportcommon.utills;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }

}

权限

权限认证使用 @PreAuthorize("hasAuthority('')")注解

总结

本文对jwt登录校验,权限管理的原理简单描述,并且提供了实现方案

相关推荐
QQ1__8115175151 天前
Spring boot名城小区物业管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·vue.js·spring boot
钛态1 天前
前端微前端架构:大项目的救命稻草还是自找麻烦?
前端·vue·react·web
一粒黑子1 天前
【实战解析】阿里开源 PageAgent:纯前端 GUI Agent,一行JS让网页支持自然语言操控
前端·javascript·开源
独角鲸网络安全实验室1 天前
2026微信小程序抓包全解析:从实操落地到合规风控,解锁前端调试新范式
前端·微信小程序·小程序·抓包·系统代理绕过·https证书严格校验·进程隔离
紫微AI1 天前
前端文本测量成了卡死一切创新的最后瓶颈,pretext实现突破了
前端·人工智能·typescript
GISer_Jing1 天前
AI前端(From豆包)
前端·aigc·ai编程
IT枫斗者1 天前
前端部署后如何判断“页面是不是最新”?一套可落地的版本检测方案(适配 Vite/Vue/React/任意 SPA)
前端·javascript·vue.js·react.js·架构·bug
测试修炼手册1 天前
[测试技术] 深入理解 JSON Web Token (JWT)
前端·json
AI老李1 天前
2026 年 Web 前端开发的 8 个趋势!
前端
里欧跑得慢1 天前
15. Web可访问性最佳实践:让每个用户都能平等访问
前端·css·flutter·web