万字详解 SpringSecurity+Mybatis前后端分离的安全管理实现(二)

万字详解 SpringSecurity+Mybatis前后端分离的安全管理实现(二),第1张

目录

1.SpringSecurity的作用

2.SpringSecurity的基本流程

3.RBAC的Dao层设计

4.SpringSecurity自定义类

1.未登录的情况

 2.验证登录

 3.鉴权 *** 作

1.获取url所需权限

2.进行权限比对 

4.注销 *** 作

5.会话失效处理

4.WebSecurityConfig


(49条消息) SpringSecurity+mybatis前后端分离的安全管理实现(一)_湘锅锅的博客-CSDN博客

在上一篇我们简单的实现了SpringSecurity与Mybatis的整合

本文主要实现俩个功能

1.基于RABC的的权限控制

2.前后端分离的登录验证

3.SpringSecurity自定义类实现分模块化

通过本文 会加深你对SpringSecurity的整个流程有了更深的了解

1.SpringSecurity的作用

认证:验证用户密码是否正确的过程
授权:对用户能访问的资源进行控制

2.SpringSecurity的基本流程

流程:

  1. 客户端发起一个请求,进入 Security 过滤器链。
  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录 *** 作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
  4. 当到 FilterSecurityInterceptor 的时候会拿到 url,根据 url 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层,否则到 AccessDeniedHandler 鉴权失败处理器处理。
3.RBAC的Dao层设计

RBAC即基于角色的访问控制。Role代表着权限的集合,一个用户对应着多个role,每个role对应着多种权限,权限对应着多种url资源。Security框架支持基于JDBC的RBAC。JDBC的访问控制可以将这些数据写入到数据库中进行持久化,代码不写死,灵活性也更高。

因此我们主表有user,role,path,还有俩张他们的关联表

/*
 Navicat Premium Data Transfer

 Source Server         : SSM
 Source Server Type    : MySQL
 Source Server Version : 80027
 Source Host           : localhost:3306
 Source Schema         : springsecurity

 Target Server Type    : MySQL
 Target Server Version : 80027
 File Encoding         : 65001

 Date: 13/05/2022 19:20:49
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_path
-- ----------------------------
DROP TABLE IF EXISTS `sys_path`;
CREATE TABLE `sys_path`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `url` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '请求路径',
  `description` varchar(128) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '路径描述',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_path
-- ----------------------------
INSERT INTO `sys_path` VALUES (1, '/level1/1', '');
INSERT INTO `sys_path` VALUES (2, '/level2/1', '');
INSERT INTO `sys_path` VALUES (3, '/level3/1', '');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `role_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '角色名',
  `role_description` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '角色说明',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'SUPER_ADMIN', NULL);
INSERT INTO `sys_role` VALUES (2, 'ADMIN', NULL);
INSERT INTO `sys_role` VALUES (3, 'ROLE_USER', NULL);

-- ----------------------------
-- Table structure for sys_role_path_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_path_relation`;
CREATE TABLE `sys_role_path_relation`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `role_id` int(0) NULL DEFAULT NULL,
  `path_id` int(0) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_role_path_relation
-- ----------------------------
INSERT INTO `sys_role_path_relation` VALUES (1, 1, 1);
INSERT INTO `sys_role_path_relation` VALUES (2, 1, 2);
INSERT INTO `sys_role_path_relation` VALUES (3, 1, 3);
INSERT INTO `sys_role_path_relation` VALUES (4, 2, 1);
INSERT INTO `sys_role_path_relation` VALUES (5, 2, 2);
INSERT INTO `sys_role_path_relation` VALUES (6, 3, 1);

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `account` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '账号',
  `user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT '用户名',
  `password` varchar(64) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '用户密码',
  `last_login_time` char(19) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '上一次登录时间',
  `enabled` bit(1) NULL DEFAULT b'1' COMMENT '账号是否可用。默认为1(可用)',
  `not_expired` bit(1) NULL DEFAULT b'1' COMMENT '是否过期。默认为1(没有过期)',
  `account_not_locked` bit(1) NULL DEFAULT b'1' COMMENT '账号是否锁定。默认为1(没有锁定)',
  `credentials_not_expired` bit(1) NULL DEFAULT b'1' COMMENT '证书(密码)是否过期。默认为1(没有过期)',
  `create_time` char(19) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '创建时间',
  `update_time` char(19) CHARACTER SET utf8 COLLATE utf8_bin NULL DEFAULT NULL COMMENT '修改时间',
  `create_user` int(0) NULL DEFAULT NULL COMMENT '创建人',
  `update_user` int(0) NULL DEFAULT NULL COMMENT '修改人',
  `deleted` bit(1) NULL DEFAULT b'0' COMMENT '逻辑删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'owen', 'admin', '7175f5c5ea79851930b990a81bac06f9', '2022-05-07 18:43', b'1', b'1', b'1', b'1', NULL, NULL, NULL, NULL, b'0');

-- ----------------------------
-- Table structure for sys_user_role_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role_relation`;
CREATE TABLE `sys_user_role_relation`  (
  `id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `user_id` int(0) NULL DEFAULT NULL COMMENT '用户id',
  `role_id` int(0) NULL DEFAULT NULL COMMENT '角色id',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

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

SET FOREIGN_KEY_CHECKS = 1;

其中路径的表根据自己需求更改就好


4.SpringSecurity自定义类 1.未登录的情况

用户都没有登陆就更别想访问我们的资源了对不对,所以当用户没有登陆的时候直接访问时,返回一个json告诉用户未登录,然后前端再去处理

因此要屏蔽框架自己的登录界面,使用AuthenticationEntryPoint接口来完成。

/**
 * 匿名用户访问无权限资源时的异常  用于屏蔽Security自带的登陆界面
 */
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("code", 199);
        map.put("msg", "用户未登录");
        map.put("success", false);
        JSONObject json = new JSONObject(map);

        response.setContentType("text/json;charset=utf-8");

        response.getWriter().write(String.valueOf(json));
    }
}

 2.验证登录

用户在知道要登陆之后,然后急匆匆开始去登录,输入账号,密码之后,那就该我们 *** 作了!!

首先,我们需要把用户输入的账号密码去验证对不对吧,需要我们拿这个信息去数据库或者内存找

如果错误,咱们再抛出异常,返回json告诉用户错误信息

如果正确,那就进入授权环节

这时候就验证需要UserDetailService接口

package com.xhx.springboot_security.sevice.impl;

import com.xhx.springboot_security.mapper.SysRoleMapper;
import com.xhx.springboot_security.mapper.SysUserMapper;
import com.xhx.springboot_security.mapper.SysUserRoleRelationMapper;
import com.xhx.springboot_security.mapper.UserMapper;
import com.xhx.springboot_security.pojo.SysUser;
import com.xhx.springboot_security.pojo.SysUserExample;
import com.xhx.springboot_security.pojo.SysUserRoleRelation;
import com.xhx.springboot_security.pojo.UserExample;
import com.xhx.springboot_security.utils.GetRolesUtil;
import com.xhx.springboot_security.utils.LoginUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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 org.springframework.ui.Model;


import javax.annotation.Resource;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class UserSevice implements UserDetailsService {

    @Autowired
    GetRolesUtil getRolesUtil;
    @Autowired
    LoginUtil loginUtil;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (username == null) {
            throw new UsernameNotFoundException("name is null!");
        }
        //查询用户的记录
        SysUser user = loginUtil.login(username);
        if (user == null) {
            throw new UsernameNotFoundException("the account: " + username + " not found!");
        }

        List roles = getRolesUtil.getroles(user.getId());//查询用户所持有的Role信息

        if (roles.size() == 0) {
            throw new UsernameNotFoundException("no role records found!");
        }


  
        
        //对于RBAC,role名称前必须加上"ROLE_"
        //框架是根据前缀是否有ROLE_来判断这是角色信息还是权限信息的。
        List authorities = roles.stream().map(r->
                new SimpleGrantedAuthority("ROLE_" + r)
        ).collect(Collectors.toList());
        
        

        //将查询到的信息封装到UserDetail的实现类:User里面。
        User sys_user = new User(username, user.getPassword(), user.getEnabled(),
                user.getNotExpired(), user.getCredentialsNotExpired(), user.getAccountNotLocked(),
                authorities);

        return sys_user;
    }
}

注意这里的user是security的自带的user

其中参数为

  1. String username:用户名
  2. String password: 密码
  3. boolean enabled: 账号是否可用
  4. boolean accountNonExpired:账号是否过期
  5. boolean credentialsNonExpired:密码是否过期
  6. boolean accountNonLocked:账号是否锁定
  7. Collection authorities):用户权限列表
     

 那其实我们发现,用户的授权已经完成了,在我们的authorities之中


如果登录失败,比如用户名是空的,密码错误等等

就需要一个异常处理类来进行失败的处理 AuthenticationFailureHandler

/**
 * 各种异常的处理 验证失败的处理
 */
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        String msg;
        if (e instanceof AccountExpiredException) {
            //账号过期
            msg = "账号过期";
        } else if (e instanceof BadCredentialsException) {
            //密码错误
            msg = "密码错误";
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            msg = "密码过期";
        } else if (e instanceof DisabledException) {
            //账号不可用
            msg = "账号不可用";
        } else if (e instanceof LockedException) {
            //账号锁定
            msg = "账号锁定";
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            msg = "用户不存在";
        }else {
            //其他错误
            msg = "其他错误";
        }

        Map map = new HashMap<>();
        map.put("code", 198);
        map.put("msg", msg);
        map.put("success", false);

        String json = JSON.toJSONString(map);

        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(json);

    }
}

那么登录失败都有回传,登录成功也一定要有哦 就需要AuthenticationSuccessHandler

/**
 * 用户身份验证成功后,返回一个json给前端,说明用户验证成功
 */
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("code", 200);
        map.put("msg", "登陆成功");
        map.put("success", true);
        String json = JSON.toJSONString(map);
        response.setContentType("text/json;charset=utf-8");

        response.getWriter().write(json);

    }
}

 3.鉴权 *** 作

用户在登录之后很高兴,自己已经获得了权限,就去到处点击url,想看看自己那些url可以访问

所以,我们就要进行鉴权 *** 作

1.获取url所需权限

一些url是需要对应的权限的,所以我们需从中数据库中根据这个url来得知如果想要访问这个url

需要哪些权限 需要FilterInvocationSecurityMetadataSource

@Component
/**
 * 拦截到当前的请求,并根据请求路径从数据库中查出当前资源路径需要哪些权限才能访问
 */
public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    GetUrlUtil getUrlUtil;
    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {
        //拿到请求地址
        String requestUrl = ((FilterInvocation)object).getRequestUrl();

        List roles = getUrlUtil.getrolesByUrl(requestUrl);

        if(roles.size() == 0){
            return null;
        }

        //查询到的role集合是一个String的List,我们将其转为String的数组
        String[] attributes = new String[roles.size()];
        int i=0;
        for(String role : roles){
            attributes[i++] = role;
        }

        //使用SecurityConfig.createList方法,将数组中的String转为ConfigAttribute对象,并形成集合返回
        return SecurityConfig.createList(attributes);

    }

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

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

2.进行权限比对 

好的,我们获取到了url需要的权限列表了这时候就要去跟用户权限进行比对

就需要AccessDecisionManager

/**
 * 根据我们的url需要哪些权限
 * 再跟用户所拥有的权限对比
 *
 */
@Component
public class CustomizeAccessDecisionManager implements AccessDecisionManager {
    @Override
    public void decide(Authentication authentication, Object object, Collection configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        AtomicBoolean flag = new AtomicBoolean(false);

        configAttributes.stream().forEach(ConfigAttribute ->{
            String needRole = "ROLE_" + ConfigAttribute.getAttribute();
            
            //用户的权限列表
            authentication.getAuthorities()
                    .stream()
                    .forEach(authority->{
                        if(authority.getAuthority().equals(needRole)){
                            flag.set(true);

                        }
                    });
        });

        if(flag.get())return;

        //没有匹配项,抛出AccessDeniedException异常
        throw new AccessDeniedException("权限不足!");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}

如果权限对的上,那就可以放行啦

如果权限对不上,那用户就无法访问,这时候需要给他返回一个json告知,所以我们就需要

AccessDeniedHandler

/**
 * 权限不足的异常处理
 */
@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("code", 403);
        map.put("msg", "权限不足");
        map.put("success", false);
        String json = JSON.toJSONString(map);
        response.setContentType("text/json;charset=utf-8");

        response.getWriter().write(json);
    }
}

至此,鉴权部分就到这里啦


4.注销 *** 作

接下来就是用户登出啦 需要LogoutSuccessHandler

/**
 * 用户身份注销成功后,返回一个json给前端,说明用户注销成功
 */
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("code", 300);
        map.put("msg", "已注销");
        map.put("success", true);
        String json = JSON.toJSONString(map);

        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(json);
    }
}
5.会话失效处理

session的默然时间是30分钟,所以30分钟后会话就失效了,所以我们还需要SessionInformationExpiredStrategy

/**
 * 会话失效的处理
 */
@Component
public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        Map map = new HashMap<>();
        map.put("code", 305);
        map.put("msg", "账号已下线");
        map.put("success", false);
        String json = JSON.toJSONString(map);

        HttpServletResponse response = event.getResponse();
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(json);
    }
}

4.WebSecurityConfig

所以的自定义类完成之后,我们需要注入到spring容器中,所以最终的WebSecurityConfig如下

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserSevice userSevice;

    //登录成功处理逻辑
    @Resource
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    //登录失败处理逻辑
    @Resource
    private AuthenticationFailureHandler authenticationFailureHandler;

    //登出成功处理逻辑
    @Resource
    private LogoutSuccessHandler LogoutSuccessHandler;


    //匿名用户访问无权限资源时的异常
    @Resource
    private AuthenticationEntryPoint authenticationEntryPoint;

    //访问决策管理器
    @Resource
    private AccessDecisionManager accessDecisionManager;

    //实现权限拦截
    @Resource
    private FilterInvocationSecurityMetadataSource securityMetadataSource;

    //会话失效(账号被挤下线)处理逻辑
    @Resource
    private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;

    //权限拒绝处理逻辑
    @Resource
    private AccessDeniedHandler accessDeniedHandler;

    // 设置默认的加密方式(MD5方式加密)
    @Resource
    private PasswordEncoder passwordEncoder;



    //权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http.authorizeRequests().
                 withObjectPostProcessor(new ObjectPostProcessor() {	//鉴权
            @Override
            public  O postProcess(O o) {
                o.setAccessDecisionManager(accessDecisionManager);//url权限和用户权限的决定器
                o.setSecurityMetadataSource(securityMetadataSource);//url权限所需角色的查询对象
                return o;
                    }
                })
                 .antMatchers("/").permitAll()
                 //登录
                 .and().formLogin().permitAll()
                .loginPage("/toLogin").loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                 //异常处理
                .and().exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPoint) //自定义的匿名用户直接访问的处理,用于屏蔽框架自带的login界面

                //注销
                .and().logout().permitAll()
                .logoutSuccessHandler(LogoutSuccessHandler).deleteCookies("JSESSIONID").invalidateHttpSession(true)
                //会话管理
                .and().authorizeRequests()
                .and().sessionManagement()
                .maximumSessions(1)	//只允许用户持有唯一的一个session,再申请会销毁之前的
                .expiredSessionStrategy(sessionInformationExpiredStrategy);	//会话失效的自定义处理

        //注销之后的跳转
        //关闭跨站访问
        http.csrf().disable();
        http.rememberMe();
    }
    //认证
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {


        auth.userDetailsService(userSevice).passwordEncoder(passwordEncoder);

    }
}

接下来就是sso 单点登录 待更............

欢迎点赞关注收藏💪💪💪

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/915988.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-16
下一篇 2022-05-16

发表评论

登录后才能评论

评论列表(0条)

保存