基于spring的安全管理框架-Spring Security

本文涉及的产品
访问控制,不限时长
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介:

基于spring的安全管理框架-Spring Security

什么是spring security?
spring security是基于spring的安全框架.它提供全面的安全性解决方案,同时在Web请求级别和调用级别确认和授权.在Spring Framework基础上,spring security充分利用了依赖注入(DI)和面向切面编程(AOP)功能,为应用系统提供声明式的安全访问控制功能,建晒了为企业安全控制编写大量重复代码的工作,是一个轻量级的安全框架,并且很好集成Spring MVC

spring security的核心功能有哪些?
1 认证 :认证用户

2 验证: 验证用户是否有哪些权限,可以做哪些事情

spring security基于哪些技术实现?
Filter,Servlet,AOP实现

框架技术准备:
IDEA 2017.3 ,MAVEN 3+ ,springboot 2.2.6 spring security 5.2.2, JDK 8+

spring security初步集成使用
创建一个基于Maven的spring boot项目,引入必需依赖

父级依赖

  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-parent</artifactId>
  <version>2.2.6.RELEASE</version>


springboot项目集成spring security的起步依赖

springboot web项目的起步依赖

  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

我们启动springboot项目的主类

大家可以看到,此刻我们已经实现了spring security最简单的功能,上面截图的最下方就是spring sceurity给我们随机生成的密码

我们此刻可以创建一个最简单的controller层来测试访问安全控制

@RestController
public class HelloController {

@RequestMapping("/sayHello")
public String sayHello() {
    System.out.println("Hello,spring security");
    return "hello,spring security";
}

}

接下来我们通过调用这个sayHello接口,我们会得到一个登录界面

此刻我们输入默认的用户名user ,密码就是控制台随机生成的一串字符 2dddf218-48c7-454c-875d-f7283e8457c1

我们就可以以成功访问:  hello,spring security

当然,我们也可以在spring的配置文件中去配置自定义的用户名和密码,这样也可以实现同样的效果,配置如下图所示.

如果我们不想使用spring security的访问控制功能,我们可以在Springboot的启动类注解上排除spring security的自动配置

@SpringBootApplication(exclude ={SecurityAutoConfiguration.class})

这样我们再次访问接口,就不会要求我们登陆就可以直接访问了.

Spring Security 基于内存配置:
去除上述所有配置,我们重新配置一个配置类去继承WebSecurityConfigurerAdapter,这个适配器类有很多方法,我们需要重写configure(AuthenticationManagerBuilder auth)方法

@Configuration //配置类
@EnableWebSecurity //启用spring security安全框架功能
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456"))
            .roles();
}

/**
 * spring security自带的加密算法PasswordEncoder,我们使用其中一种算法来对密码加密 BCryptPasswordEncoder方法采用SHA-256
 * +随机盐+密钥对密码进行加密,过程不可逆 不加密高版本会报错
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

}

这样我们就在内存配置了用户admin,密码采用加密算法去实现内存中的用户登录认证.

在实际的场景中一个用户可能有多个角色,接下来看一下基于内存角色的用户认证

首先我们在配置类上需要添加注解启用方法级别的用户角色认证@EnableGlobalMethodSecurity(prePostEnabled = true)

@Configuration //配置类
@EnableWebSecurity //启用spring security安全框架功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
//启用方法级别的认证 prePostEnabled boolean默认false,true表示可以使用 @PreAuthorize注解 和 @PostAuthorize注解
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456"))
            .roles("super", "normal");
    auth.inMemoryAuthentication().withUser("normal").password(passwordEncoder.encode("123456"))
            .roles("normal");
}

/**
 * spring security自带的加密算法PasswordEncoder,我们使用其中一种算法来对密码加密 BCryptPasswordEncoder方法采用SHA-256
 * +随机盐+密钥对密码进行加密,过程不可逆 不加密高版本会报错
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

}

此刻我们在内存中创建了两个用户,一个normal用户,只有normal权限,一个admin用户,拥有super权限和normal权限.

我们创建三个访问路径,分别对应super,normal和 super,normal都可以访问

@RequestMapping("/super")
@PreAuthorize(value = "hasRole('super')")
public String saySuper() {
    System.out.println("Hello,super!");
    return "Hello,super";
}

@RequestMapping("/normal")
@PreAuthorize(value = "hasRole('normal')")
public String sayNormal() {
    System.out.println("Hello,normal!");
    return "hello,normal";
}

@RequestMapping("/all")
@PreAuthorize(value = "hasAnyRole('normal','super')")
public String sayAll() {
    System.out.println("Hello,super,normal!");
    return "Hello,super,normal";
}

我们会发现,normal用户可以访问2,3 admin可以访问 1,2,3,由此可以看出,此刻权限控制是OK的

这样简单地基于内存的用户权限认证就完成了,但是内存中的用户信息是不稳定不可靠的,我们需要从数据库读取,那么spring security又是如何帮我们去完成的呢?

spring security基于数据库用户信息的安全访问控制
当我们把用户信息加入到数据库,需要实现框架提供的UserDetailsService接口,去通过调用数据库去获取我们需要的用户和角色信息

@Configuration //配置类
@EnableWebSecurity //启用spring security安全框架功能
@EnableGlobalMethodSecurity(prePostEnabled = true)
//启用方法级别的认证 prePostEnabled boolean默认false,true表示可以使用 @PreAuthorize注解 和 @PostAuthorize注解
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private MyUserDetailService userDetailService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    PasswordEncoder passwordEncoder = passwordEncoder();
    //        auth.inMemoryAuthentication().withUser("admin").password(passwordEncoder.encode("123456"))
    //                .roles("super", "normal");
    auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
/**
 * spring security自带的加密算法PasswordEncoder,我们使用其中一种算法来对密码加密 BCryptPasswordEncoder方法采用SHA-256
 * +随机盐+密钥对密码进行加密,过程不可逆 不加密高版本会报错
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

}

自定义实现的接口,去通过数据库查询用户信息,此处需要注意两个地方,

1:我们数据库的密码是通过new BCryptPasswordEncoder().encode("123456")生成的,明文密码是不可以的,因为我们已经指定了密码加密规则BCryptPasswordEncoder,

2:我们若有多个角色怎么办?循环遍历放入list中,注意:角色必须以ROLE_开头

@Component
public class MyUserDetailService implements UserDetailsService {

@Resource
private UserMapper userMapper;

@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    org.springframework.security.core.userdetails.User user = null;
    User userInfo = null;
    if (!StringUtils.isEmpty(userName)) {
        userInfo = userMapper.getUserInfoByName(userName);
        if (userInfo != null) {
            List<GrantedAuthority> list = new ArrayList<>();
            String role = userInfo.getRole();
            GrantedAuthority authority = new SimpleGrantedAuthority(
                    "ROLE_" + userInfo.getRole());
            list.add(authority);
            //创建User对象返回
            user = new org.springframework.security.core.userdetails.User(userInfo.getName(),
                    userInfo.getPassword(), list);
        }
    }
    return user;
}

}

这里的接口给予了用户极大的扩展空间,我们最终创建User对象返回,User对象有两个构造方法,根据需要选取,参数含义参考源码对照就行

这样我们就通过查询数据库获取用户的登录用户名和密码以及角色信息是否匹配和具有访问权限.

基于角色的权限
认证和授权:

认证(authentication):认证访问者是谁?是否是当前系统的有限用户

授权(authorization):当前用户可以做什么?

我们就以RBAC(Role-Based Access controll),这样我们就需要设计出最少五张表去完成权限控制

user 表(存储用户信息)

user_role(用户角色信息关系表)

role表(角色信息)

role_permission(角色权限信息关系表)

permission(授权信息,可以存储访问url路径等)

这样的权限设计模型,权限授予角色,角色授予用户,管理起来清晰明了

接下来我们需要再次重写MyWebSecurityConfig中的两个configure方法

我们如果想忽略控制某些资源,不加访问拦截,我们就可以在WebSecurity方法配置忽略请求的url,一般会设置登录路径,获取图形验证码路径,静态资源等

@Override

public void configure(WebSecurity web) throws Exception {
    //设置忽略拦截的路径匹配,这些请求无需拦截,直接放行
    web.ignoring().antMatchers("/index.html", "/static/**", "/login_p", "/getPicture");
}

接下来我们就重点讲一下重新的下一个方法HttpSecurity,这个方法里面配置了我们对于权限的处理

@Override

protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests() //authorizeRequests() 允许基于使用HttpServletRequest限制访问
            .withObjectPostProcessor(postProcessor())  //请求都会经过此方法配置的过滤器*****重点******,出了WebSecurity配置的忽略请求
            .and()  //返回HttpSecurity对象-----------------------------------
            .formLogin()  //指定基于表单的身份验证没指定,则将生成默认登录页面
            .loginPage("/login_p") //指定跳转登录页
            .loginProcessingUrl("/login") //登录路径
            .usernameParameter("username") //用户名参数名
            .passwordParameter("password")//密码参数名
            .failureHandler(customAuthenticationFailureHandler()) //自定义失败处理
            .successHandler(customAuthenticationSuccessHandler()) //自定义成功处理
            .permitAll().and() //返回HttpSecurity对象----------------------------------------
            .logout()//
            .logoutUrl("/logout").logoutSuccessHandler(customLogoutSuccessHandler())
            .permitAll()//
            .and()  //返回HttpSecurity对象----------------------------------------
            .csrf().disable() //默认会开启CSRF处理,判断请求是否携带了token,如果没有就拒绝访问  我们此处设置禁用
            .exceptionHandling()//
            .authenticationEntryPoint(customAuthenticationEntryPoint()) //认证入口
            .accessDeniedHandler(customAccessDeniedHandler()); //访问拒绝处理
}

public ObjectPostProcessor postProcessor() {

    ObjectPostProcessor<FilterSecurityInterceptor> obj = new ObjectPostProcessor<FilterSecurityInterceptor>() { //此方法
        @Override
        public <O extends FilterSecurityInterceptor> O postProcess(O object) {
            object.setSecurityMetadataSource(metadataSource); //通过请求地址获取改地址需要的用户角色
            object.setAccessDecisionManager(
                    accessDecisionManager); //判断是否登录,是否当前用户是否具有访问当前url的角色
            return object;
        }
    };
    return obj;
}

在这里我们需要实现两个接口FilterInvocationSecurityMetadataSource ,AccessDecisionManager

首先是FilterInvocationSecurityMetadataSource,我们在这个接口实现类里面getAttributes()方法主要做的就是获取请求路径url,然后去数据库查询哪些角色具有此路径的访问权限,然后把角色信息返回List,很巧,SecurityConfig已经提供了一个方法createList,我们直接调用此方法返回就可以

@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {

@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {

    String requestUrl = ((FilterInvocation)o).getRequestUrl();

    List<String> list = new ArrayList();
    if (list.size() > 0) {

    //伪代码 匹配到具有该url的角色放入集合

        String[] values = new String[list.size()];
        return SecurityConfig.createList(values);
    }
    //没有匹配上的资源,都是登录访问
    return SecurityConfig.createList("ROLE_LOGIN");
}

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

@Override
public boolean supports(Class<?> aClass) {
    return FilterInvocation.class.isAssignableFrom(aClass);
}

}

下面我们需要通过用户所拥有的角色和url所需角色作比对,匹配可以访问,不匹配抛出异常AccessDeniedException,这里更巧的一点是

我们可以通过Authentication获取用户所拥有的的角色,我们在上面实现类放入的角色集合也通过参数形式再次传了进来,我们可以循环比对当前用户是否有足够权限

@Component
public class UrlAccessDecisionManager implements AccessDecisionManager {

@Override
public void decide(Authentication auth, Object o, Collection<ConfigAttribute> cas){
    Iterator<ConfigAttribute> iterator = cas.iterator();
    while (iterator.hasNext()) {
        ConfigAttribute ca = iterator.next();
        //当前请求需要的权限
        String needRole = ca.getAttribute();
        if ("ROLE_LOGIN".equals(needRole)) {
            if (auth instanceof AnonymousAuthenticationToken) {
                throw new BadCredentialsException("未登录");
            } else
                return;
        }
        //当前用户所具有的权限
        Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            if (authority.getAuthority().equals(needRole)) {
                return;
            }
        }
    }
    throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
    return true;
}
@Override
public boolean supports(Class<?> aClass) {
    return true;
}

}

当我们把这两个接口自定义实现了方法之后,后面每一步的自定义处理信息,我们都可以根据业务需要去处理,比如

自定义身份验证处理器: 根据异常去响应会不同信息或者跳转url,其他自定义处理器同理

下面给大家一个处理器demo,下面自定义处理器custom**的都可以参考做不同情况处理返回值等来完成处理,前后端分离可以响应数据,不分离的可以跳转页面

public AuthenticationFailureHandler customAuthenticationFailureHandler() {

    AuthenticationFailureHandler failureHandler = new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest,
                HttpServletResponse resp, AuthenticationException e)
                throws IOException, ServletException {
            resp.setContentType("application/json;charset=utf-8");
            RespBean respBean = null;
            if (e instanceof BadCredentialsException
                    || e instanceof UsernameNotFoundException) {
                respBean = RespBean.error("账户名或者密码输入错误!");
            } else if (e instanceof LockedException) {
                respBean = RespBean.error("账户被锁定,请联系管理员!");
            } else if (e instanceof CredentialsExpiredException) {
                respBean = RespBean.error("密码过期,请联系管理员!");
            } else if (e instanceof AccountExpiredException) {
                respBean = RespBean.error("账户过期,请联系管理员!");
            } else if (e instanceof DisabledException) {
                respBean = RespBean.error("账户被禁用,请联系管理员!");
            } else {
                respBean = RespBean.error("登录失败!");
            }
            resp.setStatus(401);
            ObjectMapper om = new ObjectMapper();
            PrintWriter out = resp.getWriter();
            out.write(om.writeValueAsString(respBean));
            out.flush();
            out.close();
        }
    };
    return failureHandler;
}

当我们把表建立好,实现上面的不同接口处理器,完成上述配置,我们就可以实现安全访问控制,至于spring security更深层级的用法,欢迎大家一起探讨!有时间我会分享一下另一个主流的安全访问控制框架 Apache shiro.其实我们会发现,所有的安全框架都是基于RBAC模型来实现的,根据框架的接口去做自定义实现来完成权限控制.

原文地址https://www.cnblogs.com/zhaoletian/p/12747628.html

相关文章
|
19天前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
1月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
42 4
|
1月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
143 1
|
28天前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
35 0
|
23天前
|
前端开发 Java 数据库连接
Spring 框架:Java 开发者的春天
Spring 框架是一个功能强大的开源框架,主要用于简化 Java 企业级应用的开发,由被称为“Spring 之父”的 Rod Johnson 于 2002 年提出并创立,并由Pivotal团队维护。
42 1
Spring 框架:Java 开发者的春天
|
15天前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 构建一个前后端分离的应用框架,实现动态路由和菜单功能。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,帮助开发者提高开发效率和应用的可维护性。
34 2
|
15天前
|
消息中间件 NoSQL Java
springboot整合常用中间件框架案例
该项目是Spring Boot集成整合案例,涵盖多种中间件的使用示例,每个案例项目使用最小依赖,便于直接应用到自己的项目中。包括MyBatis、Redis、MongoDB、MQ、ES等的整合示例。
65 1
|
23天前
|
Java 数据库连接 开发者
Spring 框架:Java 开发者的春天
【10月更文挑战第27天】Spring 框架由 Rod Johnson 在 2002 年创建,旨在解决 Java 企业级开发中的复杂性问题。它通过控制反转(IOC)和面向切面的编程(AOP)等核心机制,提供了轻量级的容器和丰富的功能,支持 Web 开发、数据访问等领域,显著提高了开发效率和应用的可维护性。Spring 拥有强大的社区支持和丰富的生态系统,是 Java 开发不可或缺的工具。
|
1月前
|
NoSQL Java Redis
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
这篇文章介绍了Redis的基本命令,并展示了如何使用Netty框架直接与Redis服务器进行通信,包括设置Netty客户端、编写处理程序以及初始化Channel的完整示例代码。
42 1
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
|
29天前
|
人工智能 开发框架 Java
总计 30 万奖金,Spring AI Alibaba 应用框架挑战赛开赛
Spring AI Alibaba 应用框架挑战赛邀请广大开发者参与开源项目的共建,助力项目快速发展,掌握 AI 应用开发模式。大赛分为《支持 Spring AI Alibaba 应用可视化调试与追踪本地工具》和《基于 Flow 的 AI 编排机制设计与实现》两个赛道,总计 30 万奖金。