Spring Security

img点击并拖拽以移动

Spring Security简介

Spring Security是一个高庭自定义的安全框架。利用Spring loC/Dl和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity的原因有很多,但大部分都是发现了javaEE的 Servlet 规范或EJB规范中的安全功能缺乏典型企业应用场景。同时认识到他们在WAR或EAR级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证“和“授权”(或者访问控制)。这两点也是Spring Security重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。“授权“指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情。

快速入门

导入依赖

<!-- SpringSecurity 组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--web 组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!---Test 组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

UserDetailsService

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

PasswordEncoder密码解析器

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

接口介绍

  • encode() :把参数按照特定的解析规则进行解析
  • matches():验证从存储中获取的编码密码与编码后提交的原始密码是否匹配,则返回 true,如果不匹配,则返回 false。第一参数表示被解析的密码,第二个参数表示存储的密码。
  • upgradeEncoding ( ) :如果解析的密码能够再次解析且达到更安全的结果,则返回 true,否则返回 false,默认返回 false

img点击并拖拽以移动

测试PasswordEncoder

@SpringBootTest
class SpringsecurityDemoApplicationTests {

    @Test
    void TestPasswordEncoder() {
        //推荐使用BCryptPasswordEncoder()来实现
        PasswordEncoder pw=new BCryptPasswordEncoder();
        //加密明文密码
        String encode=pw.encode("123456");
        System.out.println(encode);

        boolean matches=pw.matches("123456",encode);
        System.out.println(matches);
    }

}

结果:
//1.
$2a$10$OuUV2MMtAHbrLggli5v9NuNWUJN/ta.xonKHQQc0B.NON0y3RpPF2
true
//2.
$2a$10$7IYwRo639Rb4MeN5hgS5Hu.eRxxfr/qPxq0f5BFSg.NR7j/Y3lXjq
true
    
 //可以发现,每次生成的密文都不一样,因为在BCryptPasswordEncoder();每次都会生成新的盐  

自定义登录逻辑

SpringSecurity配置类:

/*
* Spring Security的配置类
* */
@Configuration
public class SecurityConfig {
    //将PasswordEncoder注入容器,方便使用
    @Bean
    public PasswordEncoder getPw(){
        //返回一个PasswordEncoder实例
        return new BCryptPasswordEncoder();
    }
}

UserDetailsService的实现类

/*
* UserDetailsService的实现类
* */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private PasswordEncoder pw;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.查询传进来到的用户名(username)在数据库中是否存在,如果不存在就抛出异常:UsernameNotFoundException
        //下面先使用假数据:
        if (!"admin".equals(username)){
            throw new UsernameNotFoundException("用户名不存在");
        }
        //2.如果用户名存在,把查询出来的密码(注册时已经加密过的)进行解析,或者直接把密码放入构造方法,下面也使用假数据
        //下面这个User是SpringSecurity中的User,类似于Shiro中的Subject
        String password=pw.encode("123");
         // User构造方法的参数:
        //1.当前用户的用户名
        //1.当前用户的密码
        //1.当前用户的权限
        return new User(username,password,
                        AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
    }
}

这时启动项目,直接在SpringSecurity的内置登录页登录,如果输入用户名为admin,密码为123,登录时,就不会报用户名或者密码错误了。其他的都会报用户名或者密码错误

自定义登录页面

自定义登录页面较为简单,只需要通过继承WebSecurityConfigurerAdapter来修改它的默认登录页面

自定义登录页面:login.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username">
    密码:<input type="password" name="password">
    <input type="submit" value="登录">
</form>
</body>
</html>

主页:main.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>

登录失败页面:error.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录失败</title>
</head>
<body>
<h1><a href="/login.html">登录失败,请重新登录</a></h1>
</body>
</html>

控制类:

@Controller
public class LoginController {
   // @RequestMapping("/login")
   // public String Login(){
   //    return "redirect:main.html";
   // }

    @RequestMapping("/toMain")
    public String toMain(){
        return "redirect:main.html";
    }
}
    @RequestMapping("/toError")
    public String toError(){
        return "redirect:error.html";
    }

配置类:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //重写configure方法来自定义登录页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交
       http.formLogin()
               //登录的URL,注:必须要与咱们自定义的登录页面的表单请求的提交路径 action="路径" 一致。
               .loginProcessingUrl("/login")
               //自定义登录页面
               .loginPage("/login.html")
               //登录成功后要跳转的页面,必须是Post请求,所以不能直接写页面,要用controller做页面跳转
               //.successForwardUrl("/main.html");
               .successForwardUrl("/toMain")
              //登录失败后要跳转的页面,也是Post请求,要用controller做页面跳转
               .failureForwardUrl("/toError");

       //授权认证
        http.authorizeRequests()
                //    放行:登录错误页/error.html不需要认证
                .antMatchers("/error.html").permitAll()
                //     放行: 登录页/login.html不需要认证
                .antMatchers("/login.html").permitAll()
                //放行静态资源
                .antMatchers("/js/**","/css/**","/images/**").permitAll() 
                //放行目录下的所有png图片
                .antMatchers("/**/*.png").permitAll()
                //所有请求都必须在被认证后(登录后),才能被访问
                .anyRequest().authenticated();
             //当访问/login.html和/error.html等以外的其他请求时,会自动跳转到login.html页面

        //关闭csrf防护
        http.csrf().disable();
    }

    @Bean
    public PasswordEncoder getPw(){
        //返回一个PasswordEncoder实例
        return new BCryptPasswordEncoder();
    }
}

设置请求账号和密码的参数名

因为在SpringSecurity中,在执行自定义登录逻辑的时候,先会执行一个过滤器,登录页的用户名的参数名name必须为username,密码的参数名name必须为password,且登录请求类型必须为post,所以如果你在登录页中的用户名和密码使用了其他的参数名,过滤器便获取不到传过来的参数的值,过滤器部分代码如下:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
    //用户名的参数名
    private String usernameParameter = "username";
    //密码的参数名
    private String passwordParameter = "password";
    //只允许post请求
    private boolean postOnly = true;
    .......

如果想要自定义登录页的用户名和密码的参数名,其实也很简单,在配置类中进行如下声明:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交
       http.formLogin()
               //自定义用户名和密码的参数名
               .usernameParameter("usernameDIY")
               .passwordParameter("passwordDIY")
              
           ......
    }
}

//这样便可以在登录页面使用自定义的name参数名:
//      <form action="/login" method="post">
//        用户名:<input type="text" name="usernameDIY">
//          密码:<input type="password" name="passwordDIY">
//               <input type="submit" value="登录">
//           </form>

自定义登录成功/失败处理器

对于前后端分离项目而言,登录成功后不能直接使用controller来进行跳转,其需要一个真正的URL路径

实现AuthenticationSuccessHandler和AuthenticationFailureHandler:

1.登录成功处理器:AuthenticationSuccessHandler

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private String url;//登录成功要跳转的URL
    public MyAuthenticationSuccessHandler(String url){
        this.url=url;
    }
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.sendRedirect(url);
      /* //可以通过authentication获取当前用户的信息
        User user= (User) authentication.getPrincipal();
        System.out.println(user.getUsername());
        //考虑到安全问题,密码不会打印出来,只会打印null
        System.out.println(user.getPassword());
        System.out.println(user.getAuthorities());*/
    }
}

2.登录失败处理器:AuthenticationFailureHandler

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private String url;//登录失败要跳转的URL
    public MyAuthenticationFailureHandler(String url){
        this.url=url;
    }
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.sendRedirect(url);
    }
}

3.使用:successHandler/failureHandler


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //重写configure方法来自定义登录页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交
       http.formLogin()
                   
           .......
                  
         
         // .successForwardUrl("/toMain")
         //登录成功后的处理器,不能与successForwardUrl共存,否则会报错
         // .successHandler(new MyAuthenticationSuccessHandler("main.html"))
         .successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))//登录成功后跳转到百度
        //登录失败后的处理器,不能与successForwardUrl共存,否则会报错
        .failureHandler(new MyAuthenticationFailureHandler("error.html"));//登录后要跳转的页面,无需经过controller跳转
        //.failureForwardUrl("/toError");

            .......

    @Bean
    public PasswordEncoder getPw(){
        //返回一个PasswordEncoder实例
        return new BCryptPasswordEncoder();
    }
}

regexMatchers详解

regexMatchers与antMatchers不同的是:

antMatchers的参数是antPatterns,而regexMatchers的参数是正则表达式。

如:

        .......
http.authorizeRequests()    
        .antMatchers("/js/**","/css/**","/images/**").permitAll() 
        //放行目录下的所有png图片
        .onMatchers("/**/*.png").permitAll()
        //放行所有jpg图片
        .regexMatchers(".+[.]jpg").permitAll()
        //所有请求都必须在被认证后(登录后),才能被访问
        .anyRequest().authenticated();
 
        .......

另外,regexMatchers还有一个双参数的用户:

比如有个GET方式的controller请求:

@GetMapping("/demo")
  @ResponseBody
  public String demo(){
      return "demo";
  }
   //HttpMethod指定放行的请求的请求方式
.regexMatchers(HttpMethod.Post,"/demo").permitAll()//表示放行请求类型为Post的请求/demo
    //而controller中的 /demo 请求为GET类型,所以当我们访问/demo时也会被拦截

权限判断

hasAuthority (String)

判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建User对象时指定的,注意:权限的书写严格区分大小写

如下:给当前用户赋予admin和nomal的权限:

return new User(username,password,                     AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));

在配置类中通过hasAuthority(”admin”)设置具有admin权限才能访问:

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

hasAnyAuthority(String …)

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

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

角色判断

用户的角色也是在自定义登录逻辑中创建User对象时指定的,注意指定用户角色的固定写法ROLE_xxxxxxxx表示角色名

    return new User(username,password,                     AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_admin"));
//ROLE_admin:管理员角色

hasRole(String)

判断用户是否具有特定的角色:

//是否具有管理员admin角色
.antMatchers("/main1.html").hasRole("admin")

hasAnyRole( String… )

判断用户是否具有给定角色中的某一个的角色:

.antMatchers("/main1.html").hasAnyRole("admin","ADmin")

总结

表达式 说明
hasRole 用户具备某个角色即可访问资源
hasAnyRole 用户具备多个角色中的任意一个即可访问资源
hasAuthority 类似于 hasRole
hasAnyAuthority 类似于 hasAnyRole
permitAll 统统允许访问
denyAll 统统拒绝访问
isAnonymous 判断是否匿名用户
isAuthenticated 判断是否认证成功
isRememberMe 判断是否通过记住我登录的
isFullyAuthenticated 判断是否用户名/密码登录的
principle 当前用户
authentication 从 SecurityContext 中提取出来的用户对象

IP地址判断

指定有权访问的服务器IP地址:

.antMatchers("/main1.html").hasIpAddress("127.0.0.1")

获取IP地址可以处理器中调用getRemoteAddr()方法:

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private String url;//登录成功要跳转的URL
    public MyAuthenticationSuccessHandler(String url){
        this.url=url;
    }
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        httpServletResponse.sendRedirect(url);
        //获取ip地址--------------------------------------:
        System.out.println(httpServletRequest.getRemoteAddr());
    }
}

自定义403处理方案

第一步:实现AccessDeniedHandler

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        //设置403的状态码,SC_FORBIDDEN
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        //设置响应的请求头
        httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
        //设置无权访问时页面响应数据
        PrintWriter writer=httpServletResponse.getWriter();
        writer.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
        writer.flush();
        writer.close();
    }
}

第二步:配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;
    //重写configure方法来自定义登录页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
        .........
            
        //403处理-----------------------------------
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);
    }

        ........
}

当无权访问时,打印出如下数据:

{"status":"error","msg":"权限不足,请联系管理员"}