springboot + spring security + token + rbac 角色权限验证(前后端分离)

前言:最近做项目的时候,需要角色和权限的认证,前后端分离,查阅一番资料后,网上的大多都不符合项目的使用标准,于是自己写个博客跟大家分享一番,有什么欠缺的地方留言讨论

文章目录:

  1. 首先需要了解Spring Security的过滤链和认证流程
  2. 其次了解流程之后需要根据项目的需求做一些认证的变动(不过大体是一样的)
  3. 建立数据库脚本,引用security的配置,开始愉快的写代码了

代码放Git上了:https://github.com/lulu0008/spring-security.git

一、首先介绍Spring Security的过滤链

下边列出Spring Security过滤链默认的执行顺序

参考这位博主写的比较全面:https://blog.csdn.net/andy_zhang2007/article/details/84726992

  1.  WebAsyncManagerIntegrationFilter 【为请求处理过程中可能发生的异步调用准备安全上下文获取途径】
  2.  SecurityContextPersistenceFilter
  3. HeaderWriterFilter 【请求的处理过程中为响应对象增加一些头部信息】
  4. LogoutFilter  【退出登录】
  5. UsernamePasswordAuthenticationFilter  【默认是使用form表单登录的(可以改为其他登录方式如:json登录)】
  6. JwtAuthorizationTokenFilter【这个过滤器是验证token在项目中使用OncePerRequestFilter(保证请求只有一次经过过滤器)】
  7. RequestCacheAwareFilter 【提取请求缓存中缓存的请求】
  8. SecurityContextHolderAwareRequestFilter
  9. SessionManagementFilter
  10. ExceptionTranslationFilter
  11.  FilterSecurityInterceptor

过滤链的流程图如下

参考这位博主的:https://blog.csdn.net/zhong_csdn/article/details/79447185

在下边图片步骤主要使用步骤5和6【主要使用的是这两个过滤器实现权限的登录】

参考这位博主的图片:https://blog.csdn.net/qq_1017097573/article/details/85873125

spring security的配置文件贴出来

package com.demo.security;

import com.demo.security.filter.JwtAuthenticationProvider;
import com.demo.security.filter.JwtAuthenticationTokenFilter;
import com.demo.security.filter.MyUsernamePasswordAuthenticationFilter;
import com.demo.security.handler.*;
import com.demo.security.service.JwtUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
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.web.cors.CorsUtils;

import javax.annotation.Resource;

/**
 * 权限配置中心
 */
@Configuration
@EnableWebSecurity
//@EnableGlobalMethodSecurity(prePostEnabled = true)//是否支持web
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAuthenticationEntryPoint myAuthenticationEntryPoint;//未登陆时返回 JSON 格式的数据给前端(否则为 html)

    @Resource
    private MySuccessHandler mySuccessHandler;//自定义的登录成功处理器

    @Resource
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler; //自定义的登录失败处理器

    @Resource
    private MyLogoutSuccessHandler myLogoutSuccessHandler; //依赖注入自定义的注销成功的处理器

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;//注册没有权限的处理器

    @Resource
    private JwtUserDetailsService jwtUserDetailsService; //自定义user

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; // 拦截token JWT 拦截器

    @Resource
    private JwtAuthenticationProvider jwtAuthenticationProvider; // 自定义登录

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //这里可启用我们自己的登陆验证逻辑,用户密码加密 放到jwtAuthenticationProvider中
        //auth.userDetailsService(jwtUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
        auth.authenticationProvider(jwtAuthenticationProvider);
    }

    /**
     * 配置spring security的控制逻辑
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        String[] arrUrl = JwtAuthenticationTokenFilter.arrUrl;

        // 新加入(cors) CSRF  取消跨站请求伪造防护
        http.cors().and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 使用 JWT,关闭token

        //用户未登录
        http.httpBasic().authenticationEntryPoint(myAuthenticationEntryPoint);

        http.authorizeRequests()
                    /** 设置任何用户可以访问的路径 **/
                    .antMatchers(arrUrl).permitAll()
                    /** 解决跨域 **/
                    .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()

                    /** 任何尚未匹配的URL都只需要对用户进行身份验证  每个请求的url必须通过这个规则  RBAC 动态 url 认证 **/
                    .anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")

                /**表单登录开始配置     表单登录使用的配置  不使用暂时注释 **/
//                    .and()
//                    .formLogin() //开启登录, 定义当需要用户登录时候,转到的登录页面
//                        .loginProcessingUrl("/user/login")//loginProcessingUrl用于指定前后端分离的时候调用后台登录接口的名称
//                        .successHandler(mySuccessHandler) // 登录成功
//                        .failureHandler(myAuthenticationFailureHandler) // 登录失败
                /**表单登录结束配置     */

                    .and()
                    /** loginProcessingUrl用于指定前后端分离的时候调用后台注销接口的名称 如果启用了CSRF保护(默认),那么请求也必须是POST **/
                    .logout()
                        .logoutUrl("/logout")
                        .logoutSuccessHandler(myLogoutSuccessHandler)
                        .permitAll();

        // 无权访问 JSON 格式的数据
        http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);

        //在执行MyUsernamePasswordAuthenticationFilter之前执行jwtAuthenticationTokenFilter
        http.addFilterBefore(jwtAuthenticationTokenFilter, MyUsernamePasswordAuthenticationFilter.class);
        //用重写的Filter替换掉原有的UsernamePasswordAuthenticationFilter
   http.addFilterAt(customAuthenticationFilter(),UsernamePasswordAuthenticationFilter.class);
         // 禁用缓存
        http.headers().cacheControl();
    }


    /**
     *  JSON登陆(注册登录的bean)
     */
    @Bean
    MyUsernamePasswordAuthenticationFilter customAuthenticationFilter() throws Exception {
        MyUsernamePasswordAuthenticationFilter filter = new MyUsernamePasswordAuthenticationFilter();
        filter.setAuthenticationSuccessHandler(mySuccessHandler);
        filter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
        filter.setFilterProcessesUrl("/user/login");
        //这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
        filter.setAuthenticationManager(authenticationManagerBean());
        return filter;
    }
}

接下来就是上数据库的脚本:表设计比较简单可以后期根据自己的设计添加业务需求。

【表名统一以sys开头,角色以ROLE_(spring中建议)开头】

sys_user、sys_resource、sys_role三个主表用户、菜单、角色表。

sys_resource_role、sys_user_role(这个表可以不要,放到user表中也是可以的,不过违反的表设计的三大原则,个人感觉还是分开的好,毕竟根据业务的需求变动也好更改)两个关系表。


SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for sys_resource
-- ----------------------------
DROP TABLE IF EXISTS `sys_resource`;
CREATE TABLE `sys_resource` (
  `id` int(11) NOT NULL,
  `res_name` varchar(255) NOT NULL,
  `url` varchar(255) NOT NULL,
  `parent_id` int(11) DEFAULT NULL,
  `remark` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
  `id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `sort` int(11) DEFAULT NULL,
  `remark` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for sys_resource_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_resource_role`;
CREATE TABLE `sys_resource_role` (
  `id` int(11) NOT NULL,
  `res_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
  `id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userName` varchar(50) NOT NULL,
  `password` varchar(50) DEFAULT NULL,
  `type` varchar(10) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', 'admin', '1');
INSERT INTO `sys_user` VALUES ('2', 'user', '123456', '2');

-- ----------------------------
-- Records of sys_resource
-- ----------------------------
INSERT INTO `sys_resource` VALUES ('1', '首页', '/index', null, null);
INSERT INTO `sys_resource` VALUES ('2', '账号管理', '/user', null, null);
INSERT INTO `sys_resource` VALUES ('3', '系统管理', '/system', null, null);

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN', '1', 'ADMIN');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER', '2', 'USER');

-- ----------------------------
-- Records of sys_resource_role

-- admin权限菜单
INSERT INTO `sys_resource_role` VALUES ('1', '1', '1');
INSERT INTO `sys_resource_role` VALUES ('2', '2', '1');
INSERT INTO `sys_resource_role` VALUES ('3', '3', '1');

-- user 权限菜单
INSERT INTO `sys_resource_role` VALUES ('5', '1', '2');
INSERT INTO `sys_resource_role` VALUES ('6', '2', '2');


-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2', '2');

表设计好之后就开始新建立springboot项目了来配合使用security使用:

一、建立用户安全模型(JwtUser类)

package com.security.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.Collection;


public class JwtUser implements UserDetails, Serializable {

    private String username;
    private String password;
    //存放用户的角色信息
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser(){}

    public JwtUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    @Override
    public String toString() {
        return "JwtUser{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }

    @Override
    public String getUsername() {
        return username;
    }

    //账号是否过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    账号是否锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    ///账号凭证是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

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

    @JsonIgnore
    @Override
    public String getPassword() {
        return password;
    }

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

}

二、  token生成的工具类(JwtTokenUtil)

package com.security.utils;

import com.security.model.JwtUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

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

@Component
public class JwtTokenUtil implements Serializable {

    private static String secret = "secret";
    private static Long timeout = 60*60*2*1000L;//两小时

    /**
     * 从数据声明生成令牌
     *
     * @param claims 数据声明
     * @return 令牌
     */
    private static String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + timeout);
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    /**
     * 从令牌中获取数据声明
     *
     * @param token 令牌
     * @return 数据声明
     */
    private static Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * 生成令牌
     *
     * @param userDetails 用户
     * @return 令牌
     */
    public static String generateToken(UserDetails userDetails) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("sub", userDetails.getUsername());
        claims.put("created", new Date());
        return generateToken(claims);
    }

    /**
     * 从令牌中获取用户名
     *
     * @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;
    }

    /**
     * 判断令牌是否过期
     *
     * @param token 令牌
     * @return 是否过期
     */
    public static Boolean isTokenExpired(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            Date expiration = claims.getExpiration();
            return expiration.before(new Date());
        } catch (Exception e) {
            return false;
        }
    }

    /**
     * 刷新令牌
     *
     * @param token 原令牌
     * @return 新令牌
     */
    public static String refreshToken(String token) {
        String refreshedToken;
        try {
            Claims claims = getClaimsFromToken(token);
            claims.put("created", new Date());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            refreshedToken = null;
        }
        return refreshedToken;
    }

    /**
     * 验证令牌
     *
     * @param token       令牌
     * @param userDetails 用户
     * @return 是否有效
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        String username = getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !isTokenExpired(token));
    }

}

三、处理器(handler)。用户登录成功、失败、没有权限、用户未登录、退出登录

1.  登录成功。用户通过security的验证鉴权会交给AuthenticationSuccessHandler来处理。在这里可以做一些关于用户的统计信息,再者把token和用户的信息一起放入redis中,过期时间和生成token的时间一致即可。

package com.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.security.model.JwtUser;
import com.security.utils.JwtTokenUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 登录成功
 */
@Component
public class MySuccessHandler implements AuthenticationSuccessHandler {

    private static final Logger logger = LoggerFactory.getLogger(MySuccessHandler.class);

    /**Json转化工具*/
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        JwtUser userDetails = (JwtUser)authentication.getPrincipal();
        SecurityContextHolder.getContext().setAuthentication(authentication);
        //生成token
        String token = JwtTokenUtil.generateToken(userDetails);
        Map<String,String> map = new LinkedHashMap<>();
        map.put("code", String.valueOf(HttpServletResponse.SC_OK));
        map.put("msg", "登录成功");
        map.put("token", token);
        response.setContentType("application/json;charset=UTF-8");
        Writer writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(map));
        writer.flush();
        writer.close();
    }
}

2. 登录失败。用户都没有通过security的验证会交给 AuthenticationFailureHandler来处理。

package com.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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.Writer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 登录失败
 */
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        Map<String,String> map = new LinkedHashMap<>();
        map.put("code", "10001");
        map.put("msg", exception.getMessage());
        map.put("msg_", "登录验证失败");
        response.setContentType("application/json;charset=UTF-8");
        Writer writer = response.getWriter();
        writer.write(objectMapper.writeValueAsString(map));
        writer.flush();
        writer.close();
    }
}

3. 用户没有权限。用户都没有通过security的验证会交给 AccessdenieHandler来处理

package com.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.Writer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 没有权限处理的类
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        Map<String,String> map = new LinkedHashMap<>();
        map.put("code", String.valueOf(HttpServletResponse.SC_FORBIDDEN));
        map.put("msg", e.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        Writer writer = response.getWriter();
        try {
            writer.write(objectMapper.writeValueAsString(map));
            writer.flush();
            writer.close();
        }catch (IOException o){
            o.printStackTrace();
            if(writer != null){
                writer.flush();
                writer.close();
            }
        }
    }
}

4. 用户未登录。用户都没有通过security的验证会交给 AuthenticationEntryPoint来处理

package com.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.Writer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 用户未登录时返回给前端的数据
 */
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        Map<String,String> map = new LinkedHashMap<>();
        map.put("code", String.valueOf(HttpServletResponse.SC_CREATED));
        map.put("msg", e.getMessage());
        map.put("msg1", "用户未登录");
        response.setContentType("application/json;charset=UTF-8");
        Writer writer = response.getWriter();
        try {
            writer.write(objectMapper.writeValueAsString(map));
            writer.flush();
            writer.close();
        }catch (IOException o){
            o.printStackTrace();
            if(writer != null){
                writer.flush();
                writer.close();
            }
        }
    }
}

5.退出登录。用户都没有通过security的验证会交给 LogoutSuccessHandler来处理

package com.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.security.model.JwtUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
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.Writer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 注销登录
 */
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {

    /**Json转化工具*/
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        JwtUser userDetails = (JwtUser)authentication.getPrincipal();
        Map<String,String> map = new LinkedHashMap<>();
        map.put("code", String.valueOf(HttpServletResponse.SC_OK));
        map.put("msg", "退出成功");
        map.put("username", userDetails.getUsername());
        response.setContentType("application/json;charset=UTF-8");
        Writer writer = response.getWriter();
        try {
            writer.write(objectMapper.writeValueAsString(map));
            writer.flush();
            writer.close();
        }catch (IOException o){
            o.printStackTrace();
            if(writer != null){
                writer.flush();
                writer.close();
            }
        }
    }
}

 四、在用户的使用UsernamePasswordAuthenticationFilter 之前还需要实现UserDetailsService(根据用户的名查询用户信息,校验用户信息准备,与DB交互)

package com.security.service;


import com.security.model.JwtUser;
import com.security.model.UserModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Component;

import java.util.ArrayList;
import java.util.Collection;

/**
 *  查询用户关联角色信息
 */
@Component
public class JwtUserDetailsService implements UserDetailsService {

    private static final Logger logger = LoggerFactory.getLogger(JwtUserDetailsService.class);

//    @Autowired
//    private UserMapper userMapper;
//    @Autowired
//    private SysRoleMapper sysRoleMapper;
//    @Autowired
//    private SysUserRoleMapper sysUserRoleMapper;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //这里只是测试使用,暂时不跟数据库交互了
        UserModel user = new UserModel();
        user.setUsername(username);
        user.setPassword("admin");
        Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_ADMIN");
        //此处将权限信息添加到 GrantedAuthority 对象中,在后面进行权限验证时会使用GrantedAuthority 对象。
        grantedAuthorities.add(grantedAuthority);
        JwtUser jwtUser = new JwtUser(user.getUsername(),user.getPassword(), grantedAuthorities);
        return jwtUser;
    }
}

 五、过滤器(Filter)。

  • 自定义过滤器UsernamePasswordAuthenticationFilter 组装成security上下文中使用的Authentication
  • AbstractUserDetailsAuthenticationProvider验证用户信息
  • OncePerRequestFilter过滤器保证拦截一次(验证token)

这里可以有两种登录方式:

  1. form表单登录【spring默认的】
  2.  json登录需要在securityConfig添加自定义的MyUsernamePasswordAuthenticationFilter ,继承AbstractUserDetailsAuthenticationProvider作为登录拦截,然后在securityConfig的配置文件中引入到配置文件中使用authenticationManage重用WebSecurityConfigurerAdapter配置的AuthenticationManager配置自定义的usernamePasswordAuththenticationFilter过滤器。(使用自定义登录
package com.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.security.model.AuthenticationModel;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;

/**
 * 登录使用自定义的登录(JSON登录)
 */
public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //attempt Authentication when Content-Type is json
        if(request.getContentType().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)
                ||request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){

            //use jackson to deserialize json
            ObjectMapper mapper = new ObjectMapper();
            UsernamePasswordAuthenticationToken authRequest = null;
            try (InputStream is = request.getInputStream()){
                AuthenticationModel authenticationBean = mapper.readValue(is,AuthenticationModel.class);
                authRequest = new UsernamePasswordAuthenticationToken(
                        authenticationBean.getUsername(), authenticationBean.getPassword());
            }catch (IOException e) {
                e.printStackTrace();
                authRequest = new UsernamePasswordAuthenticationToken(
                        "", "");
            }finally {
                setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
        //transmit it to UsernamePasswordAuthenticationFilter
        else {
            return super.attemptAuthentication(request, response);
        }
    }
}

 使用AbstractUserDetailsAuthenticationProvider 拦截验证用户信息,主要实现了AuthenticationProvider的接口方法 authenticate 并提供了相关的验证逻辑。也可以使用父类AuthenticationProvider

package com.security.filter;

import com.alibaba.druid.util.StringUtils;
import com.security.model.JwtUser;
import com.security.service.JwtUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;

/**
 * 自定义拦截
 * 用户登录验证用户信息
 */
@Component
public class JwtAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {//implements AuthenticationProvider {

    @Autowired
    private JwtUserDetailsService userDetailsService;

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {

    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        //用户输入的用户名
        String username = String.valueOf(authentication.getName());
        //用户输入的密码
        String password = String.valueOf(authentication.getCredentials());
        if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)){
            throw new BadCredentialsException("用户名或密码为空!");
        }
        //通过自定义的CustomUserDetailsService,以用户输入的用户名查询用户信息
        JwtUser userDetails = (JwtUser) userDetailsService.loadUserByUsername(username);
        //用户输入密码加密后与数据库比较
        BCryptPasswordEncoder encode = new BCryptPasswordEncoder();
        if(!encode.matches(password,userDetails.getPassword())){
            throw new BadCredentialsException("密码错误!");
        }
        Object principalToReturn = userDetails;
        //将用户信息塞到SecurityContext中,方便获取当前用户信息  把当前用户信息放入Security全局缓存中
        return this.createSuccessAuthentication(principalToReturn, authentication, userDetails);
    }

    @Override
    protected UserDetails retrieveUser(String s, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
        return null;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

OncePerRequestFilter过滤器保证拦截一次(验证token)

package com.demo.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.demo.security.service.JwtUserDetailsService;
import com.demo.security.utils.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
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.io.Writer;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 确保经过filter为一次请求
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final static String HEADER = "Authorization";
    private final static String BEARER = "Bearer ";
    public static String[] arrUrl = new String[]{"/user/login"};

    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private ObjectMapper objectMapper;
    @Autowired
    private JwtUserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean boo = false;
        for(int i = 0; i < arrUrl.length; i++){
            if(antPathMatcher.match(arrUrl[i],request.getRequestURI())){
                boo = true;
                break;
            }
        }
        if(boo){
            chain.doFilter(request, response);
            return;
        }
        String header = request.getHeader(HEADER);
        if (header == null || !header.startsWith(BEARER)) {
            getResponse(response,"token不合法!");
            return;
        }
        final String authToken = header.substring(BEARER.length());
        if(JwtTokenUtil.isTokenExpired(authToken)){
            getResponse(response,"token过期!");
            return;
        }
        String username = jwtTokenUtil.getUsernameFromToken(authToken);
        if(username == null || username == ""){
            getResponse(response,"token错误!");
            return;
        }
        //把用户的信息填充到上下文中
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (userDetails != null) {
                UsernamePasswordAuthenticationToken authentication =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }
        logger.info("checking authentication " + username);
        chain.doFilter(request, response);
    }

    /**
     *  组装token验证失败的返回
     * @param res
     * @param msg
     * @return
     */
    private HttpServletResponse getResponse(HttpServletResponse res,String msg){
        Map<String,String> map = new LinkedHashMap<>();
        map.put("code", String.valueOf(HttpServletResponse.SC_FORBIDDEN));
        map.put("msg", msg);
        res.setContentType("Application/json;charset=UTF-8");
        Writer writer;
        try {
            writer = res.getWriter();
            writer.write(objectMapper.writeValueAsString(map));
            writer.flush();
            writer.close();
        }catch (Exception o){
            o.printStackTrace();
        }
        return res;
    }
}

六、RBAC自定义url的验证权限

package com.demo.security.service;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.AntPathMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service("rbacauthorityservice")
public class RbacAuthorityService {

//    @Autowired
//    private SysResourceMapper sysResourceMapper;

    /**
     * 自定义权限信息
     */
    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        //return new UsernamePasswordAuthenticationToken(userInfo, password, userInfo.getAuthorities());
        //得到的principal的信息是用户名还是整个用户信息取决于在SelfAuthenticationProvider中传参的方式
        Object userInfo = authentication.getPrincipal();
        boolean hasPermission = false;
        if (userInfo instanceof UserDetails) {
            String username = ((UserDetails) userInfo).getUsername();
            //这里不做数据库菜单路径的交互
            List<String> list = new ArrayList<>();
            list.add("/index");
            list.add("/system");
            list.add("/user");
            //自定义验证规则
            //获取当前用户的权限菜单,和请求的菜单路径做匹配
            for (int i = 0; i < list.size(); i++) {
               String role = "/api/v*" + list.get(i) + "/**";
               AntPathMatcher antPathMatcher = new AntPathMatcher();
                if (antPathMatcher.match(role, request.getRequestURI())) {
                    hasPermission = true;
                    break;
                }
            }
            return hasPermission;
        }
        return hasPermission;
    }
}

代码已经上传到git:https://github.com/lulu0008/spring-security.git

 

您好!感谢您的提问。以下是使用Spring-security jwt springboot mysql jpa完成RBAC权限管理的步骤: 1. 创建数据库表 首先,您需要在MySQL中创建以下五张表:用户表、角色表、权限表、角色权限关联表和用户角色关联表。其中,用户表和角色表是基础表,权限表是菜单配置表,角色权限关联表是角色权限之间的关联表,用户角色关联表是用户和角色之间的关联表。 2. 配置数据源和JPA或MybatisPlus 您可以选择使用JPA或者MybatisPlus来操作数据库。如果您选择JPA,那么您需要配置数据源和JPA,同时定义实体类和DAO接口;如果您选择MybatisPlus,那么您需要配置数据源和MybatisPlus,同时定义实体类和Mapper接口。 3. 配置Spring Security和JWT 接下来,您需要配置Spring Security来实现认证和授权,同时配置JWT来实现Token的生成和验证。您可以通过实现UserDetailsService接口来自定义用户认证逻辑,同时通过实现AccessDecisionManager接口来自定义授权逻辑。 4. 实现RBAC权限管理 最后,您需要实现RBAC权限管理,也就是根据用户角色权限判断用户是否有访问某个资源的权限。您可以通过定义自定义注解和拦截器来实现RBAC权限管理,同时在获取菜单列表的时候进行权限过滤。 以上就是使用Spring-security jwt springboot mysql jpa完成RBAC权限管理的步骤。感谢您的提问,希望以上内容对您有帮助!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值