Spring Security

一,简介

spring security的核心功能包括:

  • 认证(你是谁)

  • 授权(你能干什么)

  • 攻击防护(防止伪造身份)

其核心就是一组过滤链,项目启动后会自动配置。最核心的就是Basic Authenitication Filter 用来认证用户身份,一个在spring security中一种过滤器处理一种认证方式。

比如,对username,password认证过滤器来说

  • 会检查是否是一个登录请求

  • 是否包含username和password(也就是该过滤器需要对的一些认证信息)

  • 如果不满足则放行给下一个。

下一个按照自身职责判定是否是自身需要的信息,basic的特征就是在请求头中有 Authorization:Basic eHh4Onh4 的信息。中间可能还有更多的认证过滤器。最后一环是 FilterSecurityInterceptor,这里会判定该请求是否能进行访问rest服务,判断的依据是 BrowserSecurityConfig中的配置,如果被拒绝了就会抛出不同的异常(根据具体的原因)。Exception Translation Filter 会捕获抛出的错误,然后根据不同的认证方式进行信息的返回提示。

注意:绿色的过滤器可以配置是否生效,其他的都不能控制。

二,第一个项目

1,导入依赖

Spring Security已经被Spring boot进行集成,使用时直接引入启动器即可。

<!--        导入spring security依赖包-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

2,访问页面

导入spring-boot-starter-security启动器后,Spring Security已经生效,默认拦截全部请求,如果用户没有登录,则跳转内置登录页面。

在项目中新建login.html后

在浏览器输入:http://localhost:8080/login.html后会显示下面页面。

3,UserDetailsService详解

当什么也没配置时候,账号和密码都是由Spring Security定义生成的。而在实际项目中的账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现UserDetailsService接口即可,接口定义如下:

public interface UserDetailsService {
    //通过loadUserByUsername来验证登录逻辑
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

其中UserDetails是一个接口,定义如下:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); //获取所有权限
​
    String getPassword(); //获取密码
​
    String getUsername(); //获取用户名
​
    boolean isAccountNonExpired(); //账号是否过期
​
    boolean isAccountNonLocked(); //账号是否锁定
​
    boolean isCredentialsNonExpired(); 凭证(密码)是否过期。
​
    boolean isEnabled();  //判断是否可用
}

方法如下

方法详解:

1:Collection<? extends GrantedAuthority> getAuthorities();

获取用户权限,不能返回null;

2:String getPassword();

获取密码

3:String getUsername();

获取用户名

4:boolean isAccountNonExpired();

判断账户是否未过期

5:boolean isAccountNonLocked();

判断账户是否未被锁定

6:boolean isCredentialsNonExpired();

判断凭证(密码)是否未过期

7:boolean isEnabled();

账户是否启用

要想返回UserDetails的实例,就只能返回接口的实现类。Spring Security 中提供了如下实例。对于我们只需要使用里面的User类即可。User是UserDetails的实现类。要注意User的全限定路径是:

org.springframework.security.core.userdetails.User

这里常会和系统自己开发的User类弄混。

在user类中提供了很多方法和属性。

其中构造方法有两个,调用其中任何一个都可以实例化

UserDetails 实现类 User 类的实例。而三个参数的构造方法实际上也

是调用 7 个参数的构造方法。

username:用户名

password:密码

authorities:用户具有的权限。此处不允许为 null

public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
   this(username, password, true, true, true, true, authorities);
}

此处的用户名应该是客户端传递过来的用户名。而密码应该是从

数据库中查询出来的密码。Spring Security 会根据 User 中的 password

和客户端传递过来的 password 进行比较。如果相同则表示认证通过,

如果不相同表示认证失败。

authorities里面的权限对于后面学习授权是很有必要的,包含的所有内容为此用户的权限,如果里面没有包含某个权限,而在做某件事的时候必须包含某个权限则会出现403.通常都是通过AuthorityUtils.commaSeparatedStringToAuthorityList(“”)来创建authorities集合对象的,参数是用一个字符串。多个权限使用逗号来分割。

方法参数

方法参数表示用户名,此值是客户端表单传递过来的数据。默认情况下必须叫username,否则无法接收。

4,passwordEncoder密码解析器详解

Spring Security要求容器中必须有PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入PasswordEncoder的bean对象。

接口介绍

encode():把参数按照特定的解析规则进行解析(加密)。

matches()验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回true;如果不匹配,则返回false。第一个参数表示需要被解析的密码。第二个参数表示储存的密码。

boolean matches(CharSequence var1, String var2);//var1为客户端传给我们的密码,var2为加密后的密码
两个进行匹配,能配上就返回true,否则返回false。 
default boolean upgradeEncoding(String encodedPassword) {
    return false;
}  //   2次加密 ,能2次加密true,不能false

UpgradeEncoding():如果解析的密码能够再次进行解析且达到更安全的结果则返回true,否则false。默认为false。

简单演示

@Test
public void password(){
    PasswordEncoder ps =new BCryptPasswordEncoder();
    //加密
    String encode = ps.encode("231");
    System.out.println(encode);
          //比较密码
        boolean matches = ps.matches("123", encode);
        System.out.println(matches);
}
//输出之后为:$2a$10$56mEklf6Vfnug4EAYdoXl.XnGrSHQQlPLatviAInA529dnU40heLO

BCryptPasswordEncoder简介

BCryptPasswordEncoder是Spring Security官方推荐的密码解析器。

BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单向加密。可以通过strength控制加密强度,默认为10.

代码演示

    @Test
    public void test(){
        //创建解析器
        PasswordEncoder encoder =new BCryptPasswordEncoder();
        //对密码进行加密
        String password =encoder.encode("123");
        System.out.println("加密的密码:"+password);
        //判断原字符加密后的内容是否匹配
        boolean result =encoder.matches("1234",password);
        System.out.println("判断是否一致:"+result);
    }
}

5,自定义登录逻辑

进行自定义登录逻辑需要用到之前的UserDetailsService和PasswordEncoder。但是 Spring Security要求:当自定义登录逻辑时,容器内必须有PasswordEncoder实例。所以不能直接new对象。

编写配置类
@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder getPwdEncoder(){
        return new BCryptPasswordEncoder();
    }
}
自定义逻辑

在spring Security中实现UserDetailService就表示为客户详情服务。在这个类中编写用户认证逻辑。

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;
​
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //查询数据库判断用户名是否存在,如果不存在则抛出UsernameNoFoundException异常。
        if (!username.equals("admin")){  //后面更改为自定义的值或从数据库查询的值
            throw new UsernameNotFoundException("用户名不存在");
        }
        //把查询出来的密码进行解析,或直接把password放入构造方法中
        //password就是从数据库中查询出来的密码
        String password =passwordEncoder.encode("123456");
        return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}

6,自定义登录页面

修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写configure方法。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //表单认证
        http.formLogin()
                //loginProcessingUrl 登录页面表单提交地址,此地址可以不真实存在。
                .loginProcessingUrl("/login")   //当发现/login时,认为是登录,需要执行UserDetailsServiceImpl
                .successForwardUrl("/toMain")   //此处是post请求,successForwardUrl()登录成功后跳转地址
                .loginPage("/login.html") ;      //loginPage登录页面
        //url 拦截
        http.authorizeRequests()
                .antMatchers("/login.html").permitAll()  //login.html 不需要被验证  antMatchers():匹配内容   permitAll():允许
                .anyRequest().authenticated();   //所有请求必须被认证,必须登录后才能访问
​
        //关掉csrf防护
        http.csrf().disable();
    }   
    

编写控制器

当用户登录成功后跳转toMain控制器(而之前的/login 控制器方法是不执行的,所以可以直接删除)

@Controller
public class LoginController {
    @PostMapping("/toMain")
    public String toMain(){
        return "redirect:/main.html";
    }
}
失败跳转
<!DOCTYPE html> 
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title> 
</head> 
<body>
操作失败,请重新登录.
<a href="/login.html">跳转</a> 
</body> 
</html>

修改失败跳转的表单配置

对fail跳转不进行拦截

添加控制器方法

@PostMapping("fail")
public String fail(){
    return "redirect:/fail.html";
}

7,扩展:请求账户和密码的参数名

当进行登录时会执行UsernamePasswordAuthenticationFilter过滤器。

修改配置文件

修改后前端页面中form表单中的name也要修改。

控制器

使用successForwardUrl()时表示成功后转发请求到地址。内部是通过success Handler()方法进行控制成功后交给哪个类进行处理。

public FormLoginConfigurer<H> successForwardUrl(String forwardUrl) {
    this.successHandler(new ForwardAuthenticationSuccessHandler(forwardUrl));
    return this;
}

ForwardAuthenticationSuccessHandler内部就是最简单的请求转发。由于是请求转发,当遇到需要跳转到站外或者在前后端分离的项目中就无法使用。

所以当控制登录成功后去做一些事情时,可以进行自定义认证成功控制器。

自定义成功控制器

自定义类

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
​
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
       //Principal主体。存放了登录用户的信息
        Users users= (Users) authentication.getPrincipal();
        System.out.println(users.getUsername());
        System.out.println(users.getPassword());
        httpServletResponse.sendRedirect("http://www.baidu.com");
    }
}

修改配置项

使用successHandler()方法设置成功后交给哪个对象进行处理

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        //表单认证
        http.formLogin()
                //loginProcessingUrl 登录页面表单提交地址,此地址可以不真实存在。
                .loginProcessingUrl("/login")   //当发现/login时,认为是登录,需要执行UserDetailsServiceImpl
                .successHandler(new MyAuthenticationSuccessHandler())
//                .successForwardUrl("/toMain")   //此处是post请求,successForwardUrl()登录成功后跳转地址
                .failureForwardUrl("/fail")     //当登录失败时进行跳转
                .loginPage("/login.html") ;      //loginPage登录页面

同理也可以自定义失败处理器

自定义失败处理器

源码

public FormLoginConfigurer<H> failureForwardUrl(String forwardUrl) {
    this.failureHandler(new ForwardAuthenticationFailureHandler(forwardUrl));
    return this;
}

点进去

public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private final String forwardUrl;
​
    public ForwardAuthenticationFailureHandler(String forwardUrl) {
        Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl), () -> {
            return "'" + forwardUrl + "' is not a valid forward URL";
        });
        this.forwardUrl = forwardUrl;
    }
​
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
 //request作用域中设置了存储的异常对象       request.setAttribute("SPRING_SECURITY_LAST_EXCEPTION", exception);
        request.getRequestDispatcher(this.forwardUrl).forward(request, response);
    }

自定义失败处理

public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.sendRedirect("/fail.html");
    }
}

修改配置文件

三,安全深入详解

1,访问控制url匹配

在配置类中Http.authorizeRequests()主要是对url进行控制,也就是我们说的授权(访问控制)。

在所有的匹配规则中取所有规则的交集。配置顺序影响了之后的授权效果,越是具体的配置就应该放到前面。越笼统的应该放到后面。

anyRequest()

表示匹配所有的请求,设置:全部内容都需要进行认证。

antMatcher()

public C antMatchers(String... antPatterns) {
    Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
    return this.chainRequestMatchers(AbstractRequestMatcherRegistry.RequestMatchers.antMatchers(antPatterns));
}

参数是不定向参数,每个参数是一个ant表达式,用于匹配URL规则。

规则如下:

? 匹配一个字符

* 匹配0个或多个字符

** 匹配0个或多个目录

放行js文件夹下所有的脚本文件:.antMatchers("/js/**").permitAll()

放行只要是".js"的文件: antMatchers("/**/*.js").permitAll()

regexMatchers

使用正则表达式进行匹配。和antMatchers()主要的区别是参数,antMatchers()参数是ant表达式,regexMatchers()参数是正则表达式。

以.js结尾的文件都被放行:.regexMatchers(".+[.]js").permitAll()

两个参数时使用方式

无论是antMatchers()还是regexMatchers()都具有两个参数的方法,其中第一个参数都是HttpMethod,表示请求方式。当被设置后,只有特定的请求方式才执行的对应的权限设置。

public C antMatchers(String... antPatterns) {
    Assert.state(!this.anyRequestConfigured, "Can't configure antMatchers after anyRequest");
    return this.chainRequestMatchers(AbstractRequestMatcherRegistry.RequestMatchers.antMatchers(antPatterns));
}

mvcMatchers()

mvcMatchers()适用于配置了servletPath的情况。

servletPath就是所有的URL的统一前缀。在Spring整合SpringMVC的项目中可以在yml文件中添加:spring.mvc.servlet.path= /bjsxt

此时的访问控制可以写成:.mvcMatchers("demo").servletPath("/bjsxt").permitAll()

或者:antMatchers("/bjsxt/demo").permitAll()

2,内置访问控制方法介绍

Spring Security 匹配了URL后调用了permitAll()表示不需要认证,随意访问。

permitAll()表示所匹配的URL能被所有人访问

authenticated()表示所匹配的URL都需要被认证才能访问。

anonymous()表示可以匿名访问匹配的URL。

denyAll()表示所匹配的URL都不允许被访问。

rememberMe() 表示被"remember me"的用户允许访问

fullyAuthenticated() 如果用户不是被remember me 的,才能访问。

3,权限判断

用于判断用户是否具有特定的要求

hasAuthority(String)

判断用户是否具有特定的权限,用户权限是在自定义登录逻辑中创建User对象时指定的。

下图的admin就是用户的权限,需要严格区分大小写。

在配置类中通过hasMatchers("admin")设置具有admin权限时才能访问。

.antMatchers("/main1.html").hasAuthority("admin")

hasAnyAuthority(String...)

如果用户具备给定权限中的某一个,就允许访问。

给的权限严格区分大小写。

.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")

hasRole(String)

如果用户具备给定角色就允许访问。否则出现403.

参数取值源于自定义登录逻辑UserDetailsService实现类中创建User对象时给User对象时给User赋予的授权。

在给用户赋予角色时角色需要以ROLE开头,后面添加角色名称。例如ROLE_abc 其中abc是角色名,ROLE是固定字符开头。使用hasRole()时参数也只用写abc即可。否则会报错。

hasAnyRole(String...)

如果用户具备给定角色的任意一个,就允许被访问。

hasIpAddress(String)

如果请求是指定的IP就运行访问。

可以通过request.getRemoteAddr()获取 ip 地址。

需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip 地址是不一样的

四,自定义403异常处理

403异常表示无权限异常,针对此异常应当给别人看到我们自定义的画面提示,而非程序员专用画面

1,新建类AccessDeniedHandler

@Component //表示此类为一个bean
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        //SC_FORBIDDEN表示403的异常状态码
        httpServletResponse.setStatus(httpServletResponse.SC_FORBIDDEN);
        //设置返回的格式
        httpServletResponse.setHeader("Content-Type","application/json;charset/utf-8");
        PrintWriter out =httpServletResponse.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
        out.flush();
        out.close();
    }
}

2,修改配置类

配置类中重点添加异常处理器,设置访问受限后交给哪个对象进行处理。

MyAccessDeniedHandler在配置类中自动注入

//异常处理
http.exceptionHandling()
        .accessDeniedHandler(myAccessDeniedHandler);

五,底层源码分析

security 本质上是一个过滤链

1,UsernamePasswordAuthenticationFilter 过滤

字面概要:登录请求过滤:UsernamePasswordAuthenticationFilter中判断是否为post请求,只要是post请求都会被拦截到,让你进行身份验证。如果是post请求,在父类AbstractAuthenticationprocessingFilter中调用子类方法进行身份验证(查数据库,返回userDtils并封装到Authentication中,并且做session策略设置),失败做失败方法,成功进行成功的方法。

  • 9
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值