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登录校验,权限管理的原理简单描述,并且提供了实现方案

相关推荐
放下华子我只抽RuiKe55 小时前
FastAPI 全栈后端(三):数据库与 ORM
前端·数据库·react.js·oracle·性能优化·前端框架·fastapi
火山上的企鹅5 小时前
Codex实战:APP远程升级服务搭建(四)Node 服务端自动识别 APK 信息
android·服务器·git·github·qgc
可乐ea5 小时前
【Spring Boot + MyBatis|第7篇】JWT 登录认证与拦截器实现
java·spring boot·后端·mybatis·状态模式
梵得儿SHI5 小时前
Vue 项目实战与性能优化全攻略:从代码、渲染到首屏,一站式解决卡顿慢加载
前端·vue.js·性能优化·vite·前端面试·前端优化·首屏优化
ShyanZh5 小时前
【skill】HTML PPT Skill:用 Claude Code 一句话生成专业演示文稿
前端·ai·html·powerpoint·skill
JohnnyDeng945 小时前
【Android】ViewModelScope 与协程生命周期管理:告别内存泄漏,掌控异步边界
android·kotlin·mvvm·协程
AI视觉网奇5 小时前
three教学 3d资产拼接源代码
前端·css·css3
西安邮电大学5 小时前
有关栈的经典算法题
java·后端·其他·算法·面试
私人珍藏库5 小时前
【Android】瞬净ins版-无水印解析-无水印视频保存
android·app·工具·软件·多功能
Maxwellhang5 小时前
Termux 安装 Claude Code + 配置 DeepSeek API
android·智能手机