Spring Security 安全框架应用

本文深入介绍了Spring Security企业级安全框架,包括其背景、核心概念、基本架构和认证授权流程。讲解了如何配置自定义登录账号密码,使用BCrypt加密,以及实现自定义登录逻辑。同时,展示了如何通过@EnableGlobalMethodSecurity和@PreAuthorize进行权限控制。此外,还讨论了用户信息存储和Session的工作原理,最后总结了Spring Security的关键知识点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Security背景

企业中数据是最重要的资源,对于这些数据而言,有些可以直接匿名访问,有些只能登录以后才能访问,还有一些你登录成功以后,权限不够也不能访问.总之这些规则都是保护系统资源不被破坏的一种手段.几乎每个系统中都需要这样的措施对数据(资源)进行保护.我们通常会通过软件技术对这样业务进行具体的设计和实现.早期没有统一的标准,每个系统都有自己独立的设计实现,但是对于这个业务又是一个共性,后续市场上就基于共享做了具体的落地实现,
例如:Spring的Security | Apache的shiro诞生了


Security概念

Spring Security 是一个企业级安全框架,由spring官方推出,
它对软件系统中的认证,授权,加密等功能进行封装,
在springboot技术推出以后,配置方面做了很大的简化.
市场上现在的分布式架构下的安全控制逐步的都在转向使用Spring Security

Security基本架构

不难看出,Security几乎都是过滤器,其中:

  • 绿色部分为认证过滤器,需要我们自己配置,也可以配置多个认证过滤器.也可以使用Spring Security提供的默认认证过滤器
  • 黄色部分为授权过滤器.Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作

在这里插入图片描述
Security认证的授权分析
在这里插入图片描述
Security认证的登录底层调用逻辑图
在这里插入图片描述

Security依赖

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

Security入门

  1. 只需添加好依赖,即可实现网站的安全登录
  2. 在项目启动时,会自动生成了一个密码在控制台
  3. 打开浏览器输入http://localhost:8080,然后呈现登录页面
  4. 默认账号为user,密码则是项目启动时打印在控制台的密码
  5. 登录进去即可,访问该网站的所有资源
密码例如:Using generated security password: 360123aa-df93-4cd9-bab4-5212af421d2c

在这里插入图片描述

配置自定义 登录的账号&密码(初级)

一. 明文账号密码配置

1.在application.properties文件中配置

spring.security.user.name=user
spring.security.user.password=1234

2.配置完成后重启项目,即可使用配置的账号密码,即可实现登录


二. 加密账号密码配置

1.首先需要生成一个加密后密码

1.在Security中有一个专门加密的类:BCryptPasswordEncoder
2.通过该类的encode(""),即可生成bcrypt(加密方式)的密码

注意: 相同的字符串在bcrypt加密后的密码不是一样的

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String newPwd = encoder.encode("123456");

2.将用户和密码在springboot工程的配置文件中进行配置

’{bcrypt}加密后密码’:

  1. 在配置中{bcrypt}指定了密码加密时使用的算法
  2. 配置时需要注意password的前后必须带单引号
spring.security.user.name=user
spring.security.user.password='{bcrypt}$2a$10$fahHJIe3SJm3KcyiPPQ2d.a2qR029gB3qKHrKanQ87u.KbtZ6Phr.'

3.配置完成后重启项目,即可使用普通加密前的账号密码,即可实现登录

★ 说明:MD5和bcrypt加密的对比
MD5加密:

MD5加密方式特点:

  1. 一般加密都是将需要加密的内容加盐进行不可逆hash加密、
  2. md5算法对相同内容加密时都是相同的,并且不可逆
  3. md5算法对不同内容加密可能会出现相同结果,但几率很小
  4. md5的密文是32位的

实际运用

  1. 实际项目中md5盐要存储在数据库,当登录时会基于用户名,将用户信息查询出来,
  2. 基于账号属于的密码和数据库查询出盐进行hash md5加密,再与数据库存储的密码进行比对
  3. 比对结果正确,则允许登录

bcrypt加密:

1.bcrypt是对密码的一种加密方式,基于随机盐的形式进行hash不可逆加密(密文60位长度)
2.因为是随机的,所以每次生成的加密的内容也不一样,相对会更安全(密文60位长度)
3.bcrypt的密文是60位的

security实现自定义登陆逻辑

通过配置定义的缺点
SpringSecurity支持通过配置文件的方式定义用户信息(账号密码和角色等),但这种方式有明显的缺点,那就是系统上线后,用户信息的变更比较麻烦


解决策略
因此SpringSecurity也支持通过实现UserDetailsService接口的方式来提供用户认证授权信息
当写完以下配置类后,配置文件可以删除关于Security的配置

1. 创建配置类

@Configuration
public class SecurityConfig{
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

作用:
将Security框架密码加密对象(BCryptPasswordEncoder),告诉IOC容器进行管理
当security需要加密内容时,可以通过IOC取到该对象


2. 实现Security框架的UserDetailsService接口

UserDetailService 接口:

  1. 为SpringSecurity官方提供的登录逻辑处理对象,
  2. 我们自己可以实现此接口,
  3. 然后在对应的方法中进行登录逻辑的编写

该类返回类型UserDetails接口:

  1. 这里的User对象时Security内置的继承UserDetails接口
  2. 该类返回User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.
@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    	//模拟查询数据库的User表,得到的数据,如果有则继续执行; 如果没有该账号则抛出异常
        if (!"abc".equals(username)) throw new UsernameNotFoundException("账号不存在");
        //将用户传来的密码进行加密
        String encodedPwd = passwordEncoder.encode("123456");
        //创建一个Security的权限对象,模拟数据库查到的权限
        List<GrantedAuthority> grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin,ROLE_normal,sys:res:retrieve,sys:res:create");
        //该对象时Security提供的UserDetails接口的实现,用于封装用户信息,也可以基于需要自己构建UserDetails接口的实现
        //传入该对象的信息(账号,加密后的密码,和用户权限),将对象返回出去
        //该方法返回值会交给springsecurity去校验
        return new User(username,encodedPwd,grantedAuthorities);
    }
}

说明 1. 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法
username : 这个参数为页面输入的用户名(账号)
User对象 : 一般是从数据库基于用户名查询到的用户信息(该对象必须实现UserDetails接口)
权限对象: 如果权限的方式是以角色分配,编写字符串时用"ROLE_"做前缀
throws: 当该方法抛出UsernameNotFoundException异常时,则账号密码校验失败

UserDetailsService:
该类必须实现该接口,重写此方法时底层才会进行调用,该接口实现定义用户登录的逻辑和权限,对该方法返回值进行检查账号和密码,控制是否允许登录、分配权限;


注意: 需要把该类加入IOC容器

添加权限字符串语法
ROLE_admin: ROLE_是关键字,必须大写,用于设定该角色权限。后面权限可以自定义
sys:res:retrie: 自定义系统资源权限,可以按照自己需求写入权限格式

3.测试运行
当完成以上两步即可完成Security登录页面账号与密码,动态的登录访问服务器资源
权限这块,后续会详细更新


实现自定义登陆页面进行登录

1.配置放行的登录页面,及登录资源、登录失败,登录成功页面
推荐可在之前security的配置类进行页面配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//告诉系统底层在启动时,进行访问权限的初始化配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();//关闭跨域攻击,用于前后端分离的项目
        http.formLogin()//获取表单登录对象
                .loginPage("/login.html")//配置登录使用的页面(也可以是其他域名的页面)
                //设置前端发起登录请求的url地址,请求方式必须为post请求
                .loginProcessingUrl("/login")
                .usernameParameter("uname")//设置后端接收的账号参数名称,不设置时默认为username
                .passwordParameter("pass")//设置后端接收的密码参数名称,不设置时默认为password
                //设置登录成功跳转的Url地址(重定向)
                //登录失败默认跳转的是:/login.html?error
                .defaultSuccessUrl("http://www.lili.con")
                .failureUrl("http://www.baidu.com")//设置登录失败默认跳转的页面(重定向)
                //设置登录成功服务器内部转发到那个contorller接口进行业务处理(可以执行自定义功能,参数)
                //注意不可以直接填写地址,否则报405异常
                .successForwardUrl("/index.html")
                .successHandler(new RedirectAuthenticationSuccessHandler("跳转的网址"))//自定义登录成功处理器(可返回json)
                .failureHandler(new RedirectAuthenticationFailureSuccessHandler());//自定义登录失败处理器(可返回json)
		
		//获取表单注销登录对象
		http.logout()
                .logoutUrl("/logout")//设置注销时请求的地址
                .logoutSuccessUrl("/login.html");//设置访问成功跳转到那里


        //获取处理异常的对象
        http.exceptionHandling()
                //设置当用户没有登录时,security执行的处理器
                .authenticationEntryPoint(new DefaultAuthenticationEntryPoint())
                //设置角色没有权限时,security执行的处理器
                .accessDeniedHandler(new DefaultAccessDeniedException());


		//该对象的功能是放行指定的资源,或页面,不需要认证就可以访问指定的资源
        http.authorizeRequests()//拿到设置请求的资源授权的对象
        		//放行需要放行的资源,放行images下所有的资源包括子目录下所有的内容,可以指定多个
        		//允许匿名访问写上面,需要权限访问写下面
                .antMatchers("/login.html", "/images/**")
                .permitAll()//允许上面配置直接访问
                .anyRequest().authenticated();//除了以上资源,必须认证才可访问
    }
}

什么是转发?
转发又叫服务器内部转发,用户请求的接口收到消息后,该接口将直接取访问服务器内部的接口进行处理后,在将数据返回给前端请求的接口,由该接口把数据返回给前端

匹配资源授权的对象时
* 用于匹配0个或多个字符
** 用于匹配0个或多个目录及字符

@EnableGlobalMethodSecurity(prePostEnabled = true)
注解是由SpringSecurity提供(启用全局方法权限访问的安全性)
用于权限配置类,告诉系统底层在启动时,进行访问权限的初始化

注意:
1.以上表单登录对象设置的方法不是必须都写,开发时选择适合自己的设置即可;
2.该类必须实现WebSecurityConfigurerAdapter的接口
3.实现该接口的方法,需要把super.configure(http);代码删除

实现自定义Security 的各种业务处理器

说明:
现在的很多系统都采用的是前后端分离设计,我们登陆成功以后可能会跳转到前端系统的某个地址,或者返回一个json数据,我们可以自己定义登录成功的处理操作,例如

重点说明:
定义以下异常处理器后,全局异常不可捕获包含以下的异常,否则直接会被全局异常捕获而跳过以下处理器

1.自定义登录失败处理器

//实现自定义登录失败处理器
public class RedirectAuthenticationFailureSuccessHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {

    }
}

2.自定义登录成功处理器

自定义登录成功处理器必须实现AuthenticationSuccessHandler接口

//实现自定义登录成功处理器
public class RedirectAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private String redirectUrl;

    public RedirectAuthenticationSuccessHandler(String redirectUrl) {
        this.redirectUrl = redirectUrl;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.sendRedirect(redirectUrl);
    }
}

3.自定义用户权限的异常处理器(处理没有认证的访问异常)

public class DefaultAccessDeniedException implements AccessDeniedHandler {
    //此方法用于处理AccessDeniedException对象
    //@param exception: 访问被拒绝的的异常对象
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException exception) throws IOException, ServletException {

    }
}

4.自定义未认证(未登录)异常的处理器(处理没有认证的访问异常)

/**
 * 假如用户没有认证,就去访问需要认证才可以访问的资源,系统
 * 底层会抛出一个异常AuthenticationException,系统默认
 * 对此异常的处理方式是跳转到登录页面,假如现在我们不是要
 * 跳转到登录页面,而是要返回一个json格式的字符串,则需要
 * 自己定义AuthenticationEntryPoint接口的实现类
 */
public class DefaultAuthenticationEntryPoint implements AuthenticationEntryPoint {
    // 当系统出现AuthenticationException异常时,会自动调用此方法(commence-开始,着手)
    // @param exception AuthenticationException异常通知
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException exception) throws IOException, ServletException {

    }
}

对于SpringSecurity框架而言,在实现认证和授权业务时,可能出现如下两大类型异常:

  1. AuthenticationException (用户还没有认证就去访问某个需要认证才可访问的方法时,可能出现的异常,这个异常通常对应的状态码401)
  2. AccessDeniedException (用户认证以后,在访问一些没有权限的资源时,可能会出现的异常,这个异常通常对应的状态吗为403)

实现用户授权逻辑

1.确保Security 的配置类被@EnableGlobalMethodSecurity(prePostEnabled = true)注解

2.在java项目中controller层可以对每个接口设定权限,以下是代码的实现

@RestController
public class ResourceController {
    @PreAuthorize("hasRole('admin')")//设置该接口的角色权限的访问
    @RequestMapping("/doCreate")
    public String doCreate() {
        return "数据添加成功";
    }

    @RequestMapping("/doCreate")
    @PreAuthorize("hasAuthority('sys:res:create')")//设置该接口系统资源权限的访问
    public String doCreate() {
        return "数据添加成功";
    }
}

@PreAuthorize(“hasRole(‘admin’)”):
注解描述方法时: 用于告诉系统访问此方法时需要进行权限检测,需要具备指定权限才可以访问
如果没有则默认是响应前端403


hasRole(‘admin’) :
1.表示用户必须有ROLE_admin权限才可以访问此接口
2.hasRole:标志着该接口配置了角色访问权限 (检查用户的合法性)
2.接口定义权限时可直接写权限名,给对象赋予角色权限时前面必须加ROLE_跟上自定义角色权限名


hasAuthority(‘sys:res:create’)
1.表示用户必须有sys:res:create权限才可以访问此接口
2.hasAuthority: 标志着该接口配置了资源访问权限 (用户登录后的权限控制)
3.定义资源名时与给予用户的权限一致即可,可以自定义权限名


注意
如果contorller不加@PreAuthorize注解,只需要验证即可访问
如果contorller 加上@ PreAuthorize注解,如果用户有该权限才能访问

Security 对用户信息的存储

  1. 在用户登录系统时,后端Security框架调用Session技术,使服务端创建一个储存回话状态的Session对象;
  2. 当Session对象创建时还会创建一个回话Cookie对象,通过这个回话Cookie将SessionId写到客户端
  3. 客户端下次访问服务端会携带这个回话Cookie,并通过Cookie的JsessionId找到Session对象,进而获取Session对象中存储的数据

在这里插入图片描述

Security 总结知识点

1.为什么要使用Security (产生背景)

判定用户身份的合法性

2.如何理解认证?(判定用户身份的合法性)

用户密码,指纹,刷脸,刷身份证

3.如何进行身份认证?

自己写认证逻辑,借助框架去写认证逻辑-尊重框架规则

4.市场上的认证和授权框架有哪些?

SpringSecurity和Shiro框架

5.为什么会选择SpringSecurity?

功能强大,SpringBoot诞生后在配置方面做了大量的简化

6.SpringSecurity中的加密方式你用的什么?

Bcrypt,底层基于随机盐方式对密码进行hash不可逆加密,更加安全,缺陷是慢

7.为什么要进行权限控制?

防止非法用户破坏数据

8.SpringSecurity进行权限控制的步骤

通过两个注解@EnableGlobalMethodSecurity,@PreAuthorize

9.用户的信息存在哪里?用户登录成功以后,如何获取我们登录的用户信息?

后端存: session(xxxxx,user)
前端存: cookie(sessionid,xxxxx)

获取用户信息原理: 以cookie的值存储session的key,当用户登陆成功访问服务器时,服务器读取用户cookie的value,通过value查session的K获取到用户信息

拦截器+实现Security通过Token添加用户信息

//令牌拦截器,拦截客户端向服务器请求时传递的令牌
public class TokenInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.从请求中获取token对象(如何获取取决于你传递token的方式:header,params)
        String token=request.getHeader("token");
        //2.验证token是否存在
        if(token== null|| "".equals(token))  throw new RuntimeException("请先登录");//WebUtils.writeJsonToClient
        //3.验证token是否过期
        if(JwtUtils.isTokenExpired(token)) throw new RuntimeException("登录超时,请重新登录");
        //4.解析token中的认证和权限信息(一般存储在jwt格式中的负载部分)
        Claims claims= JwtUtils.getClaimsFromToken(token);
        List<String> list=(List<String>) claims.get("authorities");//这个名字应该与创建token时,指定的权限名相同

        //5.封装和存储认证和权限信息
        //5.1构建UserDetail对象(用户身份的象征-类似于一张名片,微信的二维码)
        UserDetails userDetails= User.builder()
                .username((String)claims.get("username"))
                .password("")
                .authorities(list.toArray(new String[]{}))
                .build();
        //5.2构建Security权限交互对象(记住,固定写法)
        PreAuthenticatedAuthenticationToken authToken=
                new PreAuthenticatedAuthenticationToken(
                        userDetails,//用户身份
                        userDetails.getPassword(),
                        userDetails.getAuthorities());
        //5.3将权限交互对象与当前请求进行绑定
        authToken.setDetails(new WebAuthenticationDetails(request));
        //5.4.将认证后的token存储到Security上下文(会话对象)
        SecurityContextHolder.getContext().setAuthentication(authToken);
        return true;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值