Spring Boot + Spring Security JWT权限鉴权系统

安全框架

  • Spring Boot整合Spring Security JWT实现登录认证以及权限认证

Spring Security

  • 简介

    1. SpringSecurity是一个用于Java 企业级应用程序的安全框架(简单说是对访问权限进行控制)
    2. 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)
  • 认证过程

  • Spring Security 的认证过程

    1. (前端)用户使用用户名和密码进行登录。
    2. (后端)Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。
    3. (后端)将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。
    4. (后端)AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。
    5. (后端)通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。
  • 在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定。

  • 流程展示

  • 登录流程
    Spring Boot + Spring Security JWT权限鉴权系统_第1张图片

  • 接口验证流程
    Spring Boot + Spring Security JWT权限鉴权系统_第2张图片

搭建项目

  • 技术栈

数据库: mysql 8.x
连接池: druid
持久层框架: MyBatis-plus
安全框架: Spring Security
安全传输工具: JWT
Json解析: fastjson

  • 新建Spring Boot工程
导入依赖和配置
  1. pom.xml中,引入相关依赖
 
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-starter-webartifactId>
    dependency>

    
    <dependency>
      <groupId>org.projectlombokgroupId>
      <artifactId>lombokartifactId>
    dependency>

    
    <dependency>
      <groupId>mysqlgroupId>
      <artifactId>mysql-connector-javaartifactId>
      <scope>runtimescope>
    dependency>

    
    <dependency>
      <groupId>com.baomidougroupId>
      <artifactId>mybatis-plus-boot-starterartifactId>
      <version>3.5.1version>
    dependency>

    
    <dependency>
      <groupId>com.alibabagroupId>
      <artifactId>druidartifactId>
      <version>1.2.8version>
    dependency>

    
    <dependency>
      <groupId>cn.hutoolgroupId>
      <artifactId>hutool-allartifactId>
      <version>5.3.3version>
    dependency>

    
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-starter-securityartifactId>
    dependency>

    
    <dependency>
      <groupId>com.fasterxml.jackson.coregroupId>
      <artifactId>jackson-databindartifactId>
      <version>2.11.4version>
    dependency>

    
    <dependency>
      <groupId>io.jsonwebtokengroupId>
      <artifactId>jjwtartifactId>
      <version>0.9.1version>
    dependency>

    
    <dependency>
      <groupId>org.springframework.bootgroupId>
      <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>

    <dependency>
      <groupId>org.apache.commonsgroupId>
      <artifactId>commons-lang3artifactId>
      <version>3.11version>
    dependency>

    
    <dependency>
      <groupId>io.springfoxgroupId>
      <artifactId>springfox-swagger2artifactId>
      <version>2.7.0version>
    dependency>

    
    <dependency>
      <groupId>com.github.xiaoymingroupId>
      <artifactId>swagger-bootstrap-uiartifactId>
      <version>1.9.6version>
    dependency>

    
    <dependency>
      <groupId>com.github.pengglegroupId>
      <artifactId>kaptchaartifactId>
      <version>2.3.2version>
    dependency>
  1. 配置文件 application.yml
# 开发环境配置文件
server:
  port: 9090

spring:
  #  配置数据源
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver  #数据库链接驱动
    url: jdbc:mysql://localhost:3306/base?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai #url
    username: root  #用户名
    password: root1234  #密码
    #Druid数据源配置
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 10 #初始化时建立物理连接的个数
      min-idle: 5 #最小连接池数量
      maxActive: 30 #最大连接池数量
      maxWait: 60000 #获取连接时最大等待时间,单位毫秒

# mybatis-plus 配置
mybatis-plus:
  #  配置mapper映射文件
  mapper-locations: classpath*:/mapper/*Mapper.xml
  type-aliases-package: com.fang.system.entity
  configuration:
    # 自动驼峰命名
    map-underscore-to-camel-case: true

  # mybatis sql打印(方法接口所在的包,不是mapper.xml文件所在的包)
logging:
  level:
    com.example.admin.mapper: debug

# Redis配置
  redis:
    host: 127.0.0.1    #服务器地址
    port: 6379    #服务器端口
    database: 0   #数据库
    password:   #密码
    timeout: 100000ms   #超时时间
    lettuce:
      pool:
        max-active: 1024    #最大连接数
        max-wait: 10000ms   #最大连接阻塞时间 默认-1
        max-idle: 200    #最大空闲连接
        min-idle: 5     #最小空闲连接

# jwt 配置
jwt:
  tokenHeader: Authorization
  tokenHead: Bearer # Token前缀字符
  expiration: 604800 #7天,秒单位
  secret: base-security-secret  # 密匙KEY

动态权限控制
  1. spring security的简单原理

登录验证:

用户登陆,会被AuthenticationProcessingFilter拦截,调用AuthenticationManager的实现,AuthenticationManager会调用ProviderManager来获取用户验证信息(不同的Provider调用的服务不同,因为这些信息可以是在数据库上,可以是在LDAP服务器上,可以是xml配置文件上等);

如果验证通过后会将用户的权限信息封装一个User放到spring的全局缓存SecurityContextHolder中,以备后面访问资源时使用。
访问资源(即授权管理):

访问资源(即授权管理):

访问资源(即授权管理),访问url时,会通过AbstractSecurityInterceptor拦截器拦截,其中会调用FilterInvocationSecurityMetadataSource的方法来获取被拦截url所需的全部权限,在调用授权管理器AccessDecisionManager,这个授权管理器会通过spring的全局缓存SecurityContextHolder获取用户的权限信息,还会获取被拦截的url和被拦截url所需的全部权限,配置权限策略,如果权限足够,则返回,权限不够则报错并调用权限不足页面。

  1. 数据库表设计

  2. 权限过滤器 CustomAccessFilter.java

package com.example.admin.filter;

import com.example.admin.entity.Menu;
import com.example.admin.entity.Role;
import com.example.admin.service.IMenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collection;
import java.util.List;

/**
 * 权限控制
 * @author fangqi174956
 */
@Component
public class CustomAccessFilter implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private IMenuService menuService;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        // 获取请求的url
        String requestUrl = ((FilterInvocation) object).getRequestUrl();
        List<Menu> menus = menuService.getMenusWithRole();

        for (Menu menu : menus) {
            // 判断请求url 与菜单角色是否匹配
            if(antPathMatcher.match(menu.getUrl(),requestUrl)){
                String[] roleList = menu.getRoles().stream().map(Role::getUniqueCode).toArray(String[]::new);
                return SecurityConfig.createList(roleList);
            }
        }
        // 没有匹配上的url 默认角色ROLE_login (即登录即可访问)
        return SecurityConfig.createList("ROLE_login");

    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

权限控制:判断用户角色 CustomUrlDecisionManager.java

package com.example.admin.filter;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.Collection;

/**
 * 权限控制
 * 判断用户角色
 * @author fangqi174956
 */

@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException,
            InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            // 当前url所需要的角色
            String needRole = configAttribute.getAttribute();
            // 判断角色是否是 登录即可访问的角色,此角色是在CustomAccessFilter中设置的
            if ("ROLE_login".equals(needRole)) {
                if(authentication instanceof AnonymousAuthenticationToken){
                    throw new AccessDeniedException("尚未登录,请先登录");
                }else {
                    return;
                }
            }

            // 判断角色是否为url所需要角色
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足!请联系管理员");

    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return false;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return false;
    }
}

然后需要在 SecurityConfig.java配置文件中,配置动态权限控制

  /**
     * 配置策略
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //使用jwt,不需要csrf
        http.csrf().disable()
                //基于token,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 其他所有请求都需要认证
                .anyRequest()
                .authenticated()
                // 动态权限配置
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlDecisionManager);
                        object.setSecurityMetadataSource(customAccessFilter);
                        return object;
                    }
                })
                // 禁用缓存
                .and()
                .headers()
                .cacheControl();

        //添加jwt 登录授权过滤器
        http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        //添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);
    }
开发核心类
  1. jwt-token 工具类 JwtTokenUtil.java
package com.example.admin.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * jwt-token 工具类
 * @author fangqi174956
 */

@Component
public class JwtTokenUtil {

    private static final String CLAIM_KEY_USERNAME = "sub";
    private static final String CLAIM_KEY_CREATED = "created";

    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;


    /**
     * 根据用户信息生成token
     * @param userDetails
     * @return
     */
    public String createGenerateToken(UserDetails userDetails){
        Map<String,Object> claims = new HashMap<>();
        claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername());
        claims.put(CLAIM_KEY_CREATED,new Date());
        // 调用私有方法myGenerateToken() 根据荷载生成token
        return myGenerateToken(claims);
    }

    /**
     * 根据荷载生成token
     * @param claims
     * @return
     */
    private String myGenerateToken(Map<String,Object> claims){
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(generateExpirationDate())
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    /**
     * 生成token失效时间
     * @return
     */
    private Date generateExpirationDate() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 解析token 从token中获取登录用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token){
        String username;
        try {
            Claims claims = getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 从token中获取荷载
     * @param token
     * @return
     */
    private Claims getClaimsFromToken(String token){
        Claims claims = null;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return claims;
    }

    /**
     * 验证token是否过期
     * @param token
     * @param userDetails
     * @return
     */
    public boolean validateToken(String token,UserDetails userDetails){
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token 是否失效
     * @param token
     * @return
     */
    private boolean isTokenExpired(String token) {
        Date expireDate = getExpireDateFromToken(token);
        return expireDate.before(new Date());
    }

    /**
     * 从token中获取失效时间
     * @param token
     * @return
     */
    private Date getExpireDateFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.getExpiration();
    }

    /**
     * 刷新token
     * @param token
     * @return
     */
    public String refreshToken(String token){
        Claims claims = getClaimsFromToken(token);
        claims.put(CLAIM_KEY_CREATED,new Date());
        return myGenerateToken(claims);
    }
}

  1. 自定义Authority反序列化解析器 CustomAuthorityDeserializer.java
package com.example.admin.config.security;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * 自定义Authority反序列化解析器
 * @author fangqi174956
 */
public class CustomAuthorityDeserializer extends JsonDeserializer {


    /**
     * 反序列号的方法
     * @param jsonParser
     * @param deserializationContext
     * @return
     * @throws IOException
     * @throws JsonProcessingException
     */
    @Override
    public Object deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
            throws IOException, JsonProcessingException {
        ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
        JsonNode jsonNode = mapper.readTree(jsonParser);
        List<GrantedAuthority> grantedAuthorities = new LinkedList<>();
        // 树型节点的所有元素
        Iterator<JsonNode> elements = jsonNode.elements();
        while (elements.hasNext()){
            JsonNode next = elements.next();
            JsonNode authority = next.get("authority");
            grantedAuthorities.add(new SimpleGrantedAuthority(authority.asText()));
        }
        return null;
    }
}

  1. jwt-token登录授权过滤器 JwtAuthorizationTokenFilter.java
package com.example.admin.config.security;

import cn.hutool.core.util.StrUtil;
import com.example.admin.utils.JwtTokenUtil;
import  org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;

/**
 * jwt登录授权过滤器
 * @author fangqi174956
 */
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {


    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private UserDetailsService userDetailsService;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;
    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        // tokenHeader: Authorization
        String authHeader = request.getHeader(tokenHeader);
        //存在token token="Bearer eyBdafdadfdfasdfdfasdfasd....."
        if(StrUtil.isNotBlank(authHeader) && authHeader.startsWith(tokenHead)){
            String authToken = authHeader.substring(tokenHead.length());
            //从token 中获取当前登录用户名
            String username = jwtTokenUtil.getUserNameFromToken(authToken);
            //token存在用户名但未登录
            if(StrUtil.isNotBlank(username) && SecurityContextHolder.getContext().getAuthentication() == null){
                 //登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                 //验证token是否有效,重新设置用户对象
                if(jwtTokenUtil.validateToken(authToken,userDetails)){
                    UsernamePasswordAuthenticationToken authenticationToken =
                            new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        filterChain.doFilter(request,response);
    }
}

  1. 当未登录 或者 token失效时,访问接口的自定义的返回结果
    RestAuthorizationEntryPoint.java
package com.example.admin.config.security;

import com.example.admin.common.RestResult;
import com.example.admin.constant.HttpStatusConstants;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 当未登录 或者 token失效时,访问接口的自定义的返回结果
 * @author fangqi174956
 */
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        RestResult restResult = RestResult.error("尚未登录,请登录!");
        restResult.setCode(HttpStatusConstants.CODE_401);
        out.write(new ObjectMapper().writeValueAsString(restResult));
        out.flush();
        out.close();
    }
}

  1. 当访问接口没有权限时,自定义返回结果
    RestfulAccessDeniedHandler.java
package com.example.admin.config.security;

import com.example.admin.common.RestResult;
import com.example.admin.constant.HttpStatusConstants;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 当访问接口没有权限时,自定义返回结果
 * @author fangqi174956
 */
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter out = response.getWriter();
        RestResult restResult = RestResult.error("权限不足,请联系管理员!");
        restResult.setCode(HttpStatusConstants.CODE_403);
        out.write(new ObjectMapper().writeValueAsString(restResult));
        out.flush();
        out.close();
    }
}

  1. Security核心配置类 SecurityConfig.java
package com.example.admin.config.security;

import cn.hutool.core.util.ObjectUtil;
import com.example.admin.entity.Admin;
import com.example.admin.service.IAdminService;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * SpringSecurity 配置类
 * @author fangqi174956
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * @Autowired 构造器注入
     */
    private RestAuthorizationEntryPoint restAuthorizationEntryPoint;
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
    @Autowired
    public SecurityConfig(RestAuthorizationEntryPoint restAuthorizationEntryPoint,
                          RestfulAccessDeniedHandler restfulAccessDeniedHandler) {
        this.restAuthorizationEntryPoint = restAuthorizationEntryPoint;
        this.restfulAccessDeniedHandler = restfulAccessDeniedHandler;
    }

    /**
     * 解决 无法直接注入 AuthenticationManager
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /**
     * 放行一些路径 静态资源 不走拦截链
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //设置直接放行当接口或者资源
        web.ignoring().antMatchers(
                "/captcha",
                "/redis/captcha",
                "/login",
                "/css/**",
                "/js/**",
                "/index.html",
                "favicon.ico",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources/**",
                "/v2/api-docs/**",
                "/ws/**"
        );
    }

    /**
     * 配置策略
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //使用jwt,不需要csrf
        http.csrf().disable()
                //基于token,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 其他所有请求都需要认证
                .anyRequest()
                .authenticated()
                // 禁用缓存
                .and()
                .headers()
                .cacheControl();

        //添加jwt 登录授权过滤器
        http.addFilterBefore(jwtAuthorizationTokenFilter(), UsernamePasswordAuthenticationFilter.class);

        //添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .accessDeniedHandler(restfulAccessDeniedHandler)
                .authenticationEntryPoint(restAuthorizationEntryPoint);
    }

    /**
     * 使用重写的loadUserByUsername()进行登录验证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置UserDetailsService 使用BCrypt进行密码的hash
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

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

    @Bean
    public JwtAuthorizationTokenFilter jwtAuthorizationTokenFilter(){
        return new JwtAuthorizationTokenFilter();
    }

    /**
     * 登陆身份认证
     * @return
     */
    @Bean
    @Override
    public UserDetailsService userDetailsService(){
        return new UserDetailsService() {

            @Autowired
            private IAdminService adminService;

            // 重写loadUserByUsername方法
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                System.out.println("loadUserByUsername --> " + username);
                // 根据登录用户名查询用户信息
                Admin admin = adminService.getAdminByUsername(username);
                if(ObjectUtil.isNull(admin)){
                    // 当前用户不存在
                    throw new UsernameNotFoundException("当前登录用户:" + username + "不存在!");
                }

                if(!admin.getStatus()){
                    // 当前用户已被禁用 status=false
                    throw new UsernameNotFoundException("该账号被禁用,请联系管理员!");
                }

                // 查询当前登录用户所拥有的权限(角色)
                System.out.println("=== 查询当前登录用户所拥有的权限(角色)===" + adminService.getRolesByAdminId(admin.getId()));
                admin.setRoles(adminService.getRolesByAdminId(admin.getId()));
                return admin;
            }
        };
    }

}

异常处理
  1. 全局异常处理 GlobalExceptionHandler.java
package com.example.admin.excpeption;

import com.example.admin.common.RestResult;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理
 * @author fangqi174956
 */

@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * sql 操作数据库异常
     */
    @ExceptionHandler(SQLException.class)
    public RestResult mySqlExceptionHandler(SQLException e){
        if (e instanceof SQLIntegrityConstraintViolationException) {
            return RestResult.error("该数据有关联数据,操作失败!");
        }
        return RestResult.error("数据库异常,操作失败!");
    }

    @ExceptionHandler(RuntimeException.class)
    public RestResult loginExceptionHandler(RuntimeException e){
        if(e instanceof UsernameNotFoundException){
            return RestResult.error(e.getMessage());
        }
        return RestResult.error("未知错误,请重新登录!");
    }

}

创建登录接口
  1. controller层 LoginController.java
package com.example.admin.controller.login;

import com.example.admin.common.LoginEntity;
import com.example.admin.common.RestResult;
import com.example.admin.service.IAdminService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * 登录控制器
 * @author fangqi174956
 */
@Api(tags = "【登录】Controller")
@RestController
public class LoginController {

    @Autowired
    private IAdminService adminService;

    @ApiOperation(value = "登录成功返回token")
    @PostMapping("/login")
    public RestResult login(@RequestBody LoginEntity loginEntity){
        return adminService.login(loginEntity);
    }
}

  1. service层 IAdminService.java
public interface IAdminService extends IService<Admin> {
    /**
     * 根据用户名查询用户信息
     * @param username
     * @return
     */
    Admin getAdminByUsername(String username);
    
	 /**
     * 根据adminId 查询当前登录用户所拥有的角色
     * @param adminId
     * @return List
     */
    List<Role> getRolesByAdminId(Integer adminId);
    
    /**
     * 登录成功返回token
     * @param loginEntity
     * @return
     */
    RestResult login(LoginEntity loginEntity);

	  /**
     * 获取当前登录用户信息
     * @param principal
     * @return
     */
    RestResult getAdminInfo(Principal principal);
}
  1. serviceImpl 实现类 AdminServiceImpl.java
package com.example.admin.service.impl;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.admin.common.LoginEntity;
import com.example.admin.common.RestResult;
import com.example.admin.constant.HttpStatusConstants;
import com.example.admin.entity.Admin;
import com.example.admin.entity.Role;
import com.example.admin.mapper.AdminMapper;
import com.example.admin.mapper.RoleMapper;
import com.example.admin.service.IAdminService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.admin.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
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.security.Principal;
import java.util.List;

/**
 * 

* 系统用户表 服务实现类 *

* * @author Auth:xxx * @since 2022-06-17 */
@Service public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService { @Autowired private AdminMapper adminMapper; @Autowired private RoleMapper roleMapper; @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHead}") private String tokenHead; /** * 根据用户名查询用户信息 * @param username * @return */ @Override public Admin getAdminByUsername(String username) { return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username)); } /** * 查询当前登录用户所拥有的角色 * @param adminId * @return List */ @Override public List<Role> getRolesByAdminId(Integer adminId) { return roleMapper.getRolesByAdminId(adminId); } /** * 登录成功返回token * @param loginEntity * @return */ @Override public RestResult login(LoginEntity loginEntity) { System.out.println("==== 正在验证登录 ---> " + loginEntity + "tokenHead -->" + tokenHead); // 参数校验 if(StrUtil.isBlank(loginEntity.getUsername())){ throw new UsernameNotFoundException("参数错误!"); } System.out.println("密码---> " + passwordEncoder.encode(loginEntity.getPassword())); // 登录校验 UserDetails userDetails = userDetailsService.loadUserByUsername(loginEntity.getUsername()); // 校验密码 boolean verifyPassword = passwordEncoder.matches(loginEntity.getPassword(),userDetails.getPassword()); if(!verifyPassword){ return RestResult.error("用户名或者密码不正确!"); } /** * 更新 security 登录用户对象 * 1,用户对象 : userDetails * 2,凭证(密码): 通常设置为null * 3, 用户角色(权限) : userDetails.getAuthorities() */ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 登录成功 生成token String token = jwtTokenUtil.createGenerateToken(userDetails); return RestResult.success("登录成功!", MapUtil.builder() .put("token",token) .put("tokenHead",tokenHead) .build() ); } /** * 获取当前登录用户信息 * @return */ @Override public RestResult getAdminInfo(Principal principal) { if(ObjectUtil.isNull(principal)){ return RestResult.error(HttpStatusConstants.CODE_401,"当前登录已过期,请重写登录!"); } String username = principal.getName(); Admin admin = getAdminByUsername(username); // 密码保护 admin.setPassword(null); // 获取当前登录用户拥有的权限(角色) admin.setRoles(getRolesByAdminId(admin.getId())); return RestResult.success( MapUtil.builder() .put("adminInfo",admin) .build() ); } }
  1. mapper层
    在查询当前登录用户角色时,需借助中间表 sys_admin_role
    RoleMapper.java 在mapper层中定义方法
public interface RoleMapper extends BaseMapper<Role> {
    /**
     * 查询当前登录用户所拥有的角色
     * @param adminId
     * @return List
     */
    List<Role> getRolesByAdminId(Integer adminId);
}

RoleMapper.xml中,执行sql查询方法


DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.admin.mapper.RoleMapper">

    
    <resultMap id="BaseResultMap" type="com.example.admin.entity.Role">
        <id column="id" property="id" />
        <result column="name" property="name" />
        <result column="unique_code" property="uniqueCode" />
        <result column="remark" property="remark" />
    resultMap>

    
    <sql id="Base_Column_List">
        id, name, unique_code, remark
    sql>
    
    <select id="getRolesByAdminId" resultType="com.example.admin.entity.Role">
        SELECT
            r.id,
            r.`name`,
            r.unique_code,
            r.remark
        FROM
            sys_role r
            LEFT JOIN sys_admin_role AS ar ON r.id = ar.role_id
        WHERE
            ar.admin_id = #{adminId}

    select>
mapper>

测试
  1. 登录验证
    使用swagger查看接口并测试
    Spring Boot + Spring Security JWT权限鉴权系统_第3张图片
    登录成功,结果如下
    Spring Boot + Spring Security JWT权限鉴权系统_第4张图片
    登录失败
    (1)用户名不存在
{
  "code": 500,
  "msg": "当前登录用户:tom1不存在!",
  "data": null
}

(2)当前用户被禁用

{
  "code": 500,
  "msg": "该账号被禁用,请联系管理员!",
  "data": null
}

(3) 用户名或密码错误

{
  "code": 500,
  "msg": "用户名或者密码不正确!",
  "data": null
}

鉴权:
例:用户 test --> 拥有角色 ROLE_test ;
ROLE_test 对应的菜单
[ { name: “用户管理”,url:“/sys/admin/“},{ name: “角色管理”,url:”/sys/role/”},{ name: “菜单管理”,url:“/sys/menu/**”} ]

测试结果:
登录test,测试访问接口 /sys/admin/**
Spring Boot + Spring Security JWT权限鉴权系统_第5张图片
测试访问接口 /data/file/** 该接口url不在该用户角色对应的菜单中,则无权限访问
Spring Boot + Spring Security JWT权限鉴权系统_第6张图片

你可能感兴趣的:(springboot,mybatis-plus,Spring,spring,spring,boot,java)