SpringSecurity最全实战讲解二

798 阅读1分钟

三、SpringBoot Security 快速上手

​ Spring-boot-Security: 基于Spring Boot整合的快速实现。

1、项目搭建步骤

​ 1、创建maven工程。

​ 父工程我们依然使用上面示例中的同一个父工程。

​ 创建子模块spring-boot-security pom依赖:

	<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>AuthDemo</artifactId>
        <groupId>com.tuling</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <groupId>com.tuling</groupId>
    <artifactId>spring-boot-security</artifactId>
    <version>0.0.1</version>
    <name>spring-boot-security</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</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-web</artifactId>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot-version}</version>
            </plugin>
        </plugins>
    </build>
</project>

​ 2、 在resources目录下创建application.properties。 --spring security不需要任何配置就可以直接启动

server.port=8080
spring.application.name=security-springboot

​ 3、创建启动类,注意我们在启动类中,引入了一个Spring Security提供的注解@EnableWebSecurity。

package com.tuling.springbootsecurity;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@SpringBootApplication
@EnableWebSecurity
public class SpringBootSecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSecurityApplication.class, args);
    }
}

​ 4、创建几个简单的资源访问接口

package com.tuling.springbootsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/mobile")
public class MobileController {

    @GetMapping("/query")
    public String query(){
        return "mobile";
    }
}


package com.tuling.springbootsecurity.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/salary")
public class SalaryController {

    @GetMapping("/query")
    public String query(){
        return "salary";
    }
}


到这一步呢,我们就完成了一个SpringBoot工程的基础搭建。然后我们就可以启动引用访问MobileController和SalaryController的资源了,这时就会发现,访问这两个资源会转到一个登录页面,要求先登录。登录的用户名是user,密码会在日志中打印。

2、用SpringBoot Security重新实现我们上个应用的认证和授权逻辑。

5、注入免密解析器PasswordEncoder和用户来源UserDetailsService

package com.tuling.springbootsecurity.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    //默认Url根路径跳转到/login,此url为spring security提供
    @Override
    public void addViewControllers(ViewControllerRegistry registry)
    {
        registry.addViewController("/").setViewName("redirect:/login");
    }
    /**
     * 自行注入一个PasswordEncoder。
     * @return
     */
    @Bean
    public PasswordEncoder getPassWordEncoder(){
        return new BCryptPasswordEncoder(10);
//        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 自行注入一个UserDetailsService
     * 如果没有的话,在UserDetailsServiceAutoConfiguration中会默认注入一个包含user用户的InMemoryUserDetailsManager
     * 另外也可以采用修改configure(AuthenticationManagerBuilder auth)方法并注入authenticationManagerBean的方式。
     * @return
     */
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager(User.withUsername("admin").password(passwordEncoder().encode("admin")).authorities("mobile","salary").build(),
                                                                                       User.withUsername("manager").password(passwordEncoder().encode("manager")).authorities("salary").build(),
                                                                                       User.withUsername("worker").password(passwordEncoder().encode("worker")).authorities("worker").build());
        return userDetailsManager;
//      return new JdbcUserDetailsManager(DataSource dataSource);
    }
}


6、注入校验配置规则:

package com.tuling.springbootsecurity.config;

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;

/**
 * 注入一个自定义的配置
 */
@EnableWebSecurity
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    //配置安全拦截策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //链式配置拦截策略
        http.csrf().disable()//关闭csrg跨域检查
                .authorizeRequests()
                .antMatchers("/mobile/**").hasAuthority("mobile") //配置资源权限
                .antMatchers("/salary/**").hasAuthority("salary")
                .antMatchers("/common/**").permitAll() //common下的请求直接通过
                .anyRequest().authenticated() //其他请求需要登录
                .and() //并行条件
                .formLogin().defaultSuccessUrl("/main.html").failureUrl("/common/loginFailed"); //可从默认的login页面登录,并且登录后跳转到main.html
    }
}


7、获取当前用户信息:Spring Security提供了多种获取当前用户信息的方法。

package com.tuling.springbootsecurity.controller;

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;

import javax.servlet.http.HttpServletRequest;
import java.security.Principal;

@RestController
@RequestMapping("/common")
public class LoginController {

    @GetMapping("/getLoginUserByPrincipal")
    public String getLoginUserByPrincipal(Principal principal){
        return principal.getName();
    }
    @GetMapping(value = "/getLoginUserByAuthentication")
    public String currentUserName(Authentication authentication) {
        return authentication.getName();
    }
    @GetMapping(value = "/username")
    public String currentUserNameSimple(HttpServletRequest request) {
        Principal principal = request.getUserPrincipal();
        return principal.getName();
    }
    @GetMapping("/getLoginUser")
    public String getLoginUser(){
        User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        return user.getUsername();
    }

}


然后我们把前台页面移植过来。

这样,一个简单的Spring Secuity工程就配置完成了。我们来简单总结下。

1、我们可以通过注入一个PasswordEncoder对象来实现密码加密。其中,NoOpPasswordEncoder是一个已过时的加密器,他不会对密码进行任何加密操作。而实际项目中,最常用的是BCryptPasswordEncoder。

2、我们通过注入一个UserDetailsService来管理系统的实体数据。如果我们不自己注入UserDetailsService,那在UserDetailsServiceAutoConfiguration中会默认注入一个包含user用户的UserDetailsService,user用户的密码会打印在控制台日志中。而除了我们系统中使用到的InMemoryUserDetailsManager外,SpringSecurity还提供了JdbcUserDetailsManager来实现对对数据库中的用户数据管理。

另外,关于用户数据来源,可以通过覆盖WebSecurityConfigurerAdapter中的configure(AuthenticationManagerBuilder auth)方法,并注入authenticationManagerBean()的方式进行干预。

3、目前示例中的权限规则都是从内存直接写死的,实际项目中显然都是要从数据库进行加载。而且,目前我们的规则都是基于web请求路径来定制的,而Spring Security实际上还提供了基于注解的方法级别规则配置。

3、项目测试

这样就可以启动任务进行测试了。启动后,可以访问security默认提供的登录页面 http://localhost:8080/login 在这里插入图片描述

然后就可以使用之前创建的三个用户分别登陆,登陆后进入测试主页面。 在这里插入图片描述

测试页面中,登出 使用的是Security框架提供的默认登出地址 /logout。分别访问mobile和salary下的服务可以看到权限有控制。

4、了解SpringBoot Security项目的扩展点

这样,一个基本的spring-boot-security项目就很快搭建起来了。而Spring Security实际上还提供了相当丰富的扩展点,包括用户名密码校验规则、资源校验规则、Session管理规则等。我们需要了解这些扩展点,这样才能在实际项目中,运用上Spring Security。

1、主体数据来源

​ SpringSecurity通过引用Spring容器中的UserDetailsService对象来管理主体数据。默认情况下,会注入一个包含user用户的默认主体管理服务。我们演示中就通过注入一个InMemoryUserDetailsManager对象覆盖了默认的主体管理器。

​ 实际项目中的用户信息大都会来自于数据库。在SpringSecurity中,也提供了JdbcUserDetailsManager来实现对数据库的用户信息进行管理。而如果这些不满足实际需求,可以通过自己实现一个UserDetailsService对象并注入到Spring容器中,来实现自定义的主体数据管理。

2、密码解析器

​ Spring Security提供了很多密码解析器,包括CryptPassEncoder、Argon2PasswordEncoder、Pbkdf2PasswordEncoder等,具体可以参看PassEncoder接口的实现类。其中最常用的一般就是BCryptPasswordEncoder。其中要注意的是,我们在选择不同的密码解析器后,后台存储用户密码时要存储对应的密文。

3、自定义授权及安全拦截策略

​ 最常规的方式是通过覆盖WebSecurityConfigurerAdapter中的protected void configure(HttpSecurity http)方法。通过http来配置自定义的拦截规则。包含访问控制、登录页面及逻辑、退出页面及逻辑等。

自定义登录:http.loginPage()方法配置登录页,http.loginProcessingUrl()方法定制登录逻辑。要注意的是,SpringSecurity的登录页和登录逻辑是同一个地址/login,如果使用自定义的页面,需要将登录逻辑地址也分开。例如: http.loginPage("/index.html").loginProcessingUrl("/login")。

而登录页面的一些逻辑处理,可以参考系统提供的默认登录页。但是这里依然要注意登录页的访问权限。而关于登录页的源码,可以在DefaultLoginPageGeneratingFilter中找到。

记住我功能:登录页面提供了记住我功能,此功能只需要往登录时提交一个remeber-me的参数,值可以是 on 、yes 、1 、 true,就会记住当前登录用户的token到cookie中。http.rememberMe().rememberMeParameter("remeber-me"),使用这个配置可以定制参数名。而在登出时,会清除记住我功能的cookie。

拦截策略:antMachers()方法设置路径匹配,可以用两个星号代表多层路径,一个星号代表一个或多个字符,问号代表一个字符。然后配置对应的安全策略:

​ permitAll()所有人都可以访问。denyAll()所有人都不能访问。 anonymous()只有未登录的人可以访问,已经登录的无法访问。

​ hasAuthority、hasRole这些是配置需要有对应的权限或者角色才能访问。 其中,角色就是对应一个ROLE_角色名 这样的一个资源。

另外的两个配置对象中,AuthenticationManagerBuilder配置认证策略,WebSecurity配置补充的Web请求策略。

4、关于csrf

csrf全称是Cross—Site Request Forgery 跨站点请求伪造。这是一种安全攻击手段,简单来说,就是黑客可以利用存在客户端的信息来伪造成正常客户,进行攻击。例如你访问网站A,登录后,未退出又打开一个tab页访问网站B,这时候网站B就可以利用保存在浏览器中的sessionId伪造成你的身份访问网站A。

我们在示例中是使用http.csrf().disable()方法简单的关闭了CSRF检查。而其实Spring Security针对CSRF是有一套专门的检查机制的。他的思想就是在后台的session中加入一个csrf的token值,然后向后端发送请求时,对于GET、HEAD、TRACE、OPTIONS以外的请求,例如POST、PUT、DELETE等,会要求带上这个token值进行比对。

当我们打开csrf的检查,再访问默认的登录页时,可以看到在页面的登录form表单中,是有一个name为csrf的隐藏字段的,这个就是csrf的token。例如我们在freemarker的模板语言中可以使用添加这个参数。

而在查看Spring Security后台,有一个CsrfFilter专门负责对Csrf参数进行检查。他会调用HttpSessionCsrfTokenRepository生成一个CsrfToken,并将值保存到Session中。

5、注解级别方法支持 : 在@Configuration支持的注册类上打开注解@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true)即可支持方法及的注解支持。prePostEnabled属性 对应@PreAuthorize。securedEnabled 属性支持@Secured注解,支持角色级别的权限控制。jsr250Enabled属性对应@RolesAllowed注解,等价于@Secured。

6、异常处理:现在前后端分离的状态可以使用@ControllerAdvice注入一个异常处理类,以@ExceptionHandler注解声明方法,往前端推送异常信息。