Spring Security 安全框架应用

1. Spring Security 简介

1.1 背景分析

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

1.2 Spring Security 概述

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

1.3 Spring Security 基本架构

Spring Security 在企业中实现认证和授权业务时,低层构建了大量的过滤器.
在这里插入图片描述
其中:绿色部分为认证过滤器,需要我们自己配置,也可以配置多个认证过滤器.也可以使用Spring Security提供的默认认证过滤器.黄色部分为授权过滤器.Spring Security就是通过这些过滤器然后调用相关对象一起完成认证和授权操作.

2. Spring Security 快速入门

2.1 创建工程

在这里插入图片描述

2.2 添加项目依赖

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

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

创建配置文件
在resources目录下创建application.yml文件,并指定服务端口

server:
	port:8080

2.3 创建项目启动类并运行

在这里插入图片描述
第一步:检查控制台输出的密码

Using generated security password: 800993a7-c97b-4563-8b0e-ee42d292be2c

第二步:浏览器访问localhost:8080如下显示
在这里插入图片描述
用户名:user(系统默认)
密 码:800993a7-c97b-4563-8b0e-ee42d292be2c(控制台生成)

登陆后的效果:
404不是错,是因为没有默认登录页面
在这里插入图片描述

2.4 自定义登陆成功页面

在项目的resources目录下创建static目录,并在此目录创建一个index.html文件
在这里插入图片描述
重启服务再次登录访问测试,登录成功后系统默认跳转到index.html页面
https://img-blog.csdnimg.cn/fc102bb42d4641eb83c845ecf047f388.png

2.5 配置登录密码

在application.yml文件中可配置用户名和密码

spring:
  security:
    user:
      name: DJH
      password: ghhyw

查看application.yml文件可以看到密码,可以采用密码加密算法生成密码,如MD5, BCrypt,如下:
在这里插入图片描述
在这里插入图片描述
其中,{bcrypt}是指定密码加密时使用的算法

3. Spring Security 认证逻辑实现

自定义登录逻辑

Spring Security支持通过配置文件的方式定义用户信息(账号密码和角色等),但这种方式有明显的缺点,就是系统上线后,用户信息变更比较麻烦.因此Spring Security还支持通过实现UserDetailsService接口的方式来提供用户认证授权信息,其应用过程如下:
第一步:定义security配置类

/**
 * 由@Configuration注解描述的类为spring中的配置类,配置类会在spring
 * 工程启动时优先加载,在配置类中通常会对第三方资源进行初始配置.
 */
@Configuration
public class SecurityConfig {
    /**
     * 定义SpringSecurity密码加密对象
     * @Bean 注解通常会在@Configuration注解描述的类中描述方法,
     * 用于告诉spring框架这个方法的返回值会交给spring管理,并spring
     * 管理的这个对象起个默认的名字,这个名字与方法名相同,当然也可以通过
     * @Bean注解起名字
     */
    @Bean("bcryptPasswordEncoder")//自定义名字,不写默认就是方法名
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

第二步:定义UserDetailService接口实现类,自定义登录逻辑,代码如下:
UserDetailService为Spring Security官方提供的登录逻辑处理对象,我们可以实现此接口,然后对应的方法中进行登录逻辑编写

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    /**
     * 当我们执行登录操作时,底层会通过过滤器等对象,调用这个方法.
     *
     * @param username 这个参数为页面输出的用户名
     * @return  一般是从数据库基于用户名查询到的用户信息
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        //1.基于用户名从数据库查询用户信息
        if(!"jack".equals(username))
            throw new UsernameNotFoundException("user no exists");
        //2.将用户信息封装到UserDetails对象中并返回
        //假设这个秘密是数据库查询的加密的
        String encodePwd = passwordEncoder.encode("123456");
        List<GrantedAuthority> grantedAuthorityList = AuthorityUtils
                .commaSeparatedStringToAuthorityList(
                        "ROLE_jp, ROLE_tongpai, sys:res:retrieve, sys:res:create");
        User user = new User(username, encodePwd, grantedAuthorityList);
        return user;
    }
}

说明,这里的User对象会交给SpringSecurity框架,框架提取出密码信息,然后与用户输入的密码进行匹配校验.

第三步:启动服务进行登陆,访问测试。

自定义登录页面

第一步:在static目录下创建login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <img src="../images/a.jpg">
    <form action="/login" method="post">
        <ul>
            <li>username:</li>
            <li><input type="text" name="username"></li>
            <li>password:</li>
            <li><input type="password" name="password"></li>
        </ul>
        <li><input type="submit" value="Sign in"></li>
    </form>
</body>
</html>

注意:请求的url暂时为”/login”,请求方式必须为post方式,请求的参数暂时必须为username,password。这些规则默认在UsernamePasswordAuthenticationFilter中进行了定义。

第二步:修改安全配置类,让其实现接口,并重写相关config方法

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //对http请求的安全控制进行配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //1.关闭跨域攻击
        http.csrf().disable();
        //2.配置登录url(登录表单使用那个页面)
        http.formLogin()
                //设置登陆页面
                .loginPage("/login.html")
                //设置登陆请求处理地址(对应form表单中的action),登陆时会访问UserDetailService对象
                .loginProcessingUrl("/login")
                //设置请求用户名参数为username(默认就是username,可以自己修改,需要与表单同步)
                //.usernameParameter("username")
                //请求请求密码参数为password(默认就是password,可以自己修改,需要与表单同步)
                //.passwordParameter("password")
                //设置登陆成功跳转页面(默认为/index.html)
                .defaultSuccessUrl("/index.html")
                //.successForwardUrl("/index.html")//转发
//                .successHandler(
//                        new RedirectAuthenticationSuccessHandler(
//                                "localhost:8080/index.html")
//                        );
                //.failureHandler();
                .failureUrl("/login.html?wocuole");

        // 3.放行登录url(不许要认证就可以访问)
        http.authorizeRequests()
                .antMatchers("/login.html", "/images/**")//写放行的资源
                .permitAll()//允许直接访问
                .anyRequest().authenticated();//除了上面的资源必须认证才能访问
    }
}

登陆成功和失败处理器

定义登录成功处理器

//实现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);
    }
}

放出静态资源

http.authorizeRequests()
                .antMatchers("/login.html", "/images/**")//写放行的资源
                .permitAll()//允许直接访问
                .anyRequest().authenticated();//除了上面的资源必须认证才能访问
  • *用于匹配0个或多个字符
  • **用于匹配0个或多个目录及字符

登出设计及实现

在SecurityManager配置类中的configure(HttpSecurity http)方法中,添加登出配置,例如

 http.logout()       //开始设置登出信息
        .logoutUrl("/logout")   //登出路径
        .logoutSuccessUrl("/login.html?logout");//设置登出后显示的页面

4. Spring Security 授权逻辑实现

修改授权配置类

在权限配置类上添加启用全局方法访问控制注解

/**
 * @EnableGlobalMethodSecurity 注解由SpringSecurity提供,
 * 用于描述权限配置类,告诉系统底层在启动时,进行访问权限的初始化配置
 * 1)Enable-启用
 * 2)Global-全局
 * 3)Method-方法
 * 4)Security-安全
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	.....
}

定义Controller

@PreAuthorize注解描述方法时,用于告诉系统访问此方法时需要进行权限检测。需要具备指定权限才可以访问。例如:

@PreAuthorize(“hasAuthority('sys:res:delete”) 需要具备sys:res:delete权限
@PreAuthorize(“hasRole(‘admin’)”) 需要具备admin角色

package com.djh.jt.security.controller;

import com.sun.org.apache.regexp.internal.RE;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 可以将这里的Controller看成是系统内部的一个资源对象,
 * 我们要求访问此对象中的方法时需要进行权限检查
 *
 */
@RestController
public class ResourceController {
    /**
     * 添加操作
     * @PreAuthorize 注解由SpringSecurity框架提供,用于描述方法,此注解描述
     * 方法以后,再访问方法首先要进行权限检测
     */
    //@PreAuthorize("hasRole('jt')")
    @PreAuthorize("hasAuthority('sys:res:create')")
    @RequestMapping("/doCreate")
    public String doCreate() {
        return "create resource (insert data) ok";
    }
    /**查询操作*/
    @PreAuthorize("hasAuthority('sys:res:retrieve')")
    @RequestMapping("/doRetrieve")
    public String doRetrieve() {
        return "retrieve resource (insert data) ok";
    }
    /**修改操作*/
    @PreAuthorize("hasAuthority('sys:res:update')")
    @RequestMapping("/doUpdate")
    public String doUpdate() {
        return "update resource (insert data) ok";
    }
    /**删除操作*/
    @PreAuthorize("hasAuthority('sys:res:delete')")
    @RequestMapping("/doDelete")
    public String doDelete() {
        return "delete resource (insert data) ok";
    }
}

启动服务访问测试

使用不同用户进行登录,然后执行资源访问,假如没有权限,则会看到响应状态代码为403

在这里插入图片描述

5. SpringSecuilty认证和授权异常处理

异常类型

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

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

SpringSecuilty框架给了默认的异常处理方式,当默认的异常处理方式不满足我们实际业务的需求时,此时我们就要自定义异常处理逻辑,编写逻辑时需要遵循如下规范:

  • AuthenticationEntryPoint:统一处理 AuthenticationException 异常
  • AccessDeniedHandler:统一处理 AccessDeniedException 异常

自定义异常处理对象

 处理没有认证的访问异常
package com.djh.jt.security.config.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

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

/**
 * 假如用户没有认证,就去访问需要认证才可以访问的资源,
 * 底层会抛出一个异常AuthenticationException,
 * 系统对此异常的处理方式是跳转到登录页面,假如现在我们
 * 不要跳转到登录页,而是需要返回一个json格式的字符串,
 * 则需要自己定义AuthenticationEntryPoint接口的
 * 实现类
 */
public class DefaulAuthenticationEntryPoint implements AuthenticationEntryPoint {

    /**
     * 当系统出现AuthenticationException异常时,
     * 会自己动调用此方法(commence-开始)
     * @param httpServletRequest    请求对象
     * @param httpServletResponse   响应对象
     * @param exception                     异常
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void commence(HttpServletRequest httpServletRequest,
                         HttpServletResponse httpServletResponse,
                         AuthenticationException exception) throws IOException, ServletException {

        //页面跳转,访问失败直接跳到某个地址
        //方案一:重定向
        //httpServletResponse.sendRedirect("www.baidu.com");

        //方案二:假如访问被拒绝了向客户端响应一个json格式的字符串
        //2.1 设置响应数据的编码
        httpServletResponse.setCharacterEncoding("utf-8");
        //2.2 告诉浏览器响应数据的内容类型以及编码
        httpServletResponse.setContentType("application/json,charset=utf-8");
        //2.3 获取输出流对象
        PrintWriter out = httpServletResponse.getWriter();
        //2.4 将数据输出到客户端
        //2.4.1 封装数据
        Map<String, Object> map = new HashMap<>();
        map.put("state",
                HttpServletResponse.SC_UNAUTHORIZED);
        map.put("message",
                "请先登录在访问");
        // 2.4.2 将数据转换为json字符串,并输出数据
        out.println(new ObjectMapper().writeValueAsString(map));
        out.flush();

    }
}
处理没有权限时抛出的异常
package com.djh.jt.security.config.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

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

/**
 * 默认访问拒绝异常处理器
 * Default 默认
 * Access 访问
 * Denied 拒绝
 * Exception 异常
 * Handler 处理器
 */
public class DefaultAccessDeniedExceptionHandler implements AccessDeniedHandler {

    /**
     * 此方法用于处理AccessDeniedExceotion对象
     * @param httpServletRequest  请求对象
     * @param httpServletResponse   响应对象
     * @param exception 访问被拒绝的异常
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void handle(
            HttpServletRequest httpServletRequest,
            HttpServletResponse httpServletResponse,
            AccessDeniedException exception) throws IOException, ServletException {
        //页面跳转,访问失败直接跳到某个地址
        //方案一:重定向
        //httpServletResponse.sendRedirect("www.baidu.com");

        //方案二:假如访问被拒绝了向客户端响应一个json格式的字符串
        //2.1 设置响应数据的编码
        httpServletResponse.setCharacterEncoding("utf-8");
        //2.2 告诉浏览器响应数据的内容类型以及编码
        httpServletResponse.setContentType("application/json,charset=utf-8");
        //2.3 获取输出流对象
        PrintWriter out = httpServletResponse.getWriter();
        //2.4 将数据输出到客户端
        //2.4.1 封装数据
        Map<String, Object> map = new HashMap<>();
        map.put("state", HttpServletResponse.SC_FORBIDDEN);
        map.put("message", "没有访问权限,请联系管理员");
        // 2.4.2 将数据转换为json字符串,并输出数据
        out.println(new ObjectMapper().writeValueAsString(map));
        out.flush();
    }
}

配置异常处理对象

在配置类SecurityConfig中添加自定义异常处理对象,代码如下:

http.exceptionHandling()
            .authenticationEntryPoint(new DefaultAuthenticationEntryPoint())
            .accessDeniedHandler(new DefaultAccessDeniedExceptionHandler());
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卑微前端汪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值