SpringSecurity 学习(二)

SpringSecurity

1. antMatchers()
  1. 方法定义:

    public C antMatchers(String... antPatterns)
    

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

    规则如下:
    ?: 匹配一个字符
    *: 匹配0个活多个字符
    **: 匹配0个活多个目录

    在实际项目中我们经常要放行所有静态资源,如放行js文件夹下的所有脚本。

    .antMatchers("/js/**").perimitAll()
    

    还有一种配置方式是只要是.js文件都放行

    .antMatchers("/**/*.js").perimitAll()
    
    1. 方法定义
    public C antMathchers(HttpMethod method, String... antPatterns)
    

    指定请求方式,只有匹配ant表达式,并且用指定请求方式时候,请求才会被放行。否则,请求将被拦截。

2. regexMatchers()

通过正则表达式匹配。

3. mvcMatchers()

通过在项目的配置文件中增加配置:

spring.mvm.servlet.path=/API

可以为所有请求的Controller增加统一前缀/API,访问所有Controller的时候,需要带上/API前缀,才能访问到。如果我们要在项目中放行特定的资源,不被拦截,那么可以这样写:

.mvcMatchers("/demo").servletPath("/API").perimitAll() 

放行了 /API/demo这个请求。

4. Spring Security访问控制

从源码 ExpressionUrlAuthorizationConfigurer 中可以看到

	static final String permitAll = "permitAll";   // 放行所有请求

	private static final String denyAll = "denyAll"; // 拒绝所有请求

	private static final String anonymous = "anonymous"; // 匿名访问,与permitAll差不多,但是请求会走特定的拦截器

	private static final String authenticated = "authenticated"; // 需要认证

	private static final String fullyAuthenticated = "fullyAuthenticated"; // 需要完全认证

	private static final String rememberMe = "rememberMe"; // 记住我,下次访问可以直接访问

Spring Security 有6中访问控制。

5. hasAuthority 和 hasAnyAuthority

判断用户是否具有特定权限

6. hasRole 和 hasAnyRole

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

7. hasIpAddress

通过指定的Ip地址,进行访问控制。

8. 自定义403页面

当操作被禁止后,会返回到403页面,而Spring Security提供的403页面不够友好,此时,我们可以自定义403页面。

  1. 实现AccessDeniedHandler接口

    @Componente
    public class MyAccessDeniedHandler  implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setHeader("Content-Type", "application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"status\":\"error\", \"msg\": \"has no authority, please concact us\"}");
            writer.flush();
            writer.close();
        }
    }
    
  2. 修改configure方法

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        private MyAccessDeniedHandler myAccessDeniedHandler;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()
                    .usernameParameter("username123")
                    .passwordParameter("password123")
                    .loginProcessingUrl("/login")
                    .loginPage("/login.html")
                            //.successForwardUrl("/toMain")
                    //.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
                    .successHandler(new MyAuthenticationSuccessHandler("/main.html")) // 登录成功后,到达的页面
                            //.failureForwardUrl("/toError");
                    .failureHandler(new MyAuthenticationFailureHandler("/error.html"));
    
            http.authorizeRequests()
                    .antMatchers("/error.html").permitAll()
                    .antMatchers("/login.html").permitAll()
                    .antMatchers("/main1.html").hasAuthority("abc")
                    .anyRequest().authenticated();
    
            http.csrf().disable();
    
            http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); // 当操作被禁止后,执行的handler
        }
    
        @Bean
        public PasswordEncoder getPwd() {
            return new BCryptPasswordEncoder();
        }
    }
    

    main.html页面

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    main.html <a href="/main1.html">jump to main1.html</a>
    </body>
    </html>
    

    到达main.html页面后,点击jump to main1.html连接,跳转会出现:

    {“status”:“error”, “msg”: “has no authorization”}

    此时,说明我们自定义的跳转逻辑生效了。.antMatchers("/main1.html").hasAuthority(“abc”) 判断用户是否有abc权限,如果没有,就会执行代码 http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler); 就会执行我们自定义的myAccessDeniedHandler的逻辑。

9. 基于表达式的访问控制

通过看 permitAll 和 hasAuthority 方法的源码:

public ExpressionInterceptUrlRegistry permitAll() {
	return access(permitAll);
}
public ExpressionInterceptUrlRegistry hasAuthority(String authority) {
	return access(ExpressionUrlAuthorizationConfigurer.hasAuthority(authority));
}		

注意到,最终都是调用access方法。因此,我们也可以改写我们前面的config方法,实现同样的访问控制。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .usernameParameter("username123")
                .passwordParameter("password123")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                        //.successForwardUrl("/toMain")
                //.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
                .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
                        //.failureForwardUrl("/toError");
                .failureHandler(new MyAuthenticationFailureHandler("/error.html"));

        http.authorizeRequests()
                //.antMatchers("/error.html").permitAll()
                .antMatchers("/error.html").access("permitAll()")  // 基于表达式的访问控制
                //.antMatchers("/login.html").permitAll()
                .antMatchers("/login.html").access("permitAll()")
                // .antMatchers("/main1.html").hasAuthority("abc")
                .antMatchers("/main1.html").access("hasAuthority('abc')") 
                .anyRequest().authenticated();

        http.csrf().disable();

        http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
    }

Spring Security 支持的表达式有:

  • hashRole(role)
  • hasAnyRole([role1,role2,…])
  • hasAuthority([authority])
  • hasAnyAuthority([authority, authority2,…])
  • principal
  • authentication
  • permitAll
  • denyAll
  • isAnonymous()
  • isRememberMe()
  • isAuthenticated()
  • isFullyAuthenticated()
  • hasPermission(Object target, Object permission)
10. 基于注解的访问控制

在Spring Security中提供了一些访问控制的注解。这些注解默认都是不可用的,需要通过@EnableGlobalMethodSecurity进行开启后使用,该注解标注在启动类或 继承了WebSecurityConfigurerAdapter 的配置类上。

  • @EnableGlobalMethodSecurity(securedEnabled = true)
    会激活@Secured,开启基于角色注解的访问控制,将注解@Secured(“ROLE_角色”) 标注在Controller的方法上,那么只有有相应角色的用户才能够访问该方法。
  • @EnableGlobalMethodSecurity(prePostEnabled = true)
    会激活@PreAuthorize和@PostAuthorize,这两个注解都是方法或类级别的注解。
    • @PreAuthorize表示访问方法或类在执行之前先判断权限,大多情况下都是使用这个注解,注解的参数和access()方法参数值相同,都是权限表达式。
      如@PreAuthorize(“hasRole(‘ROLE_abc’)”) 表示拥有abc角色可以访问,但是也可以在写成@PreAuthorize(“hasRole(‘abc’)”) 但是如果是在 configure方法中使用access表达式配置,角色前面不能以ROLE_开头
    • @PostAuthorize 表示方法或类执行结束后判断权限,此注解很少被使用。
11. RememberMe功能实现

Spring Security 中Remember Me 为“记住我”功能,用户只需要在登录时添加remember-me复选框,取值为true。Spring Security会自动把用户信息存储到数据源中,以后就可以不登录进行访问了。

  1. 添加依赖
    Spring Security实现Remember Me功能时底层实现依赖Spring-JDBC,所以需要导入Spring-JDBC。以后多使用MyBatis框架而很少直接导入Spring-jdbc,所以此处导入mybatis启动器同时还需要添加MySQL驱动。
       <dependency>
           <groupId>org.mybatis.spring.boot</groupId>
           <artifactId>mybatis-spring-boot-starter</artifactId>
           <version>2.1.1</version>
       </dependency>
    
       <dependency>
           <groupId>mysql</groupId>
           <artifactId>mysql-connector-java</artifactId>
           <version>8.0.19</version>
       </dependency>
    
  2. 配置application.properties
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://ip:port/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    spring.datasource.username=root
    spring.datasource.password=password
    
  3. 在mysql中创建数据库并指定编码
    create database security CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
    
  4. 在configure方法中增加rememberMe的逻辑
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Autowired
        private MyAccessDeniedHandler myAccessDeniedHandler;
    
        @Autowired
        private UserDetailsService userDetailsService;
        
        @Autowired
        private DataSource dataSource;
        
        @Autowired
        private PersistentTokenRepository persistentTokenRepository;
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin()
                    .usernameParameter("username123")
                    .passwordParameter("password123")
                    .loginProcessingUrl("/login")
                    .loginPage("/login.html")
                            //.successForwardUrl("/toMain")
                    //.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
                    .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
                            //.failureForwardUrl("/toError");
                    .failureHandler(new MyAuthenticationFailureHandler("/error.html"));
    
            http.authorizeRequests()
                    //.antMatchers("/error.html").permitAll()
                    .antMatchers("/error.html").access("permitAll()")
                    //.antMatchers("/login.html").permitAll()
                    .antMatchers("/login.html").access("permitAll()")
                    // .antMatchers("/main1.html").hasAuthority("abc")
                    .antMatchers("/main1.html").access("hasAuthority('abc')")
                    .anyRequest().authenticated();
    
            http.csrf().disable();
    
            http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);
            
            http.rememberMe()
                    // 自定义登录逻辑
                    .userDetailsService(userDetailsService)
                    // 持久层对象
                    .tokenRepository(persistentTokenRepository);
        }
    
        @Bean
        public PasswordEncoder getPwd() {
            return new BCryptPasswordEncoder();
        }
        
        @Bean
        public PersistentTokenRepository getPersistentTokenRepository() {
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            jdbcTokenRepository.setDataSource(dataSource);
            // 这样代码只需要在第一次启动时,存在。其他时候需要注释掉,否则会报错
            jdbcTokenRepository.setCreateTableOnStartup(true);
            return jdbcTokenRepository;
        }
    }
    
  5. login.html 页面
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <form action="/login" method="post">
            Username:<input type="text" name="username123"/><br/>
            Password: <input type="password" name="password123"/><br/>
            Remember Me: <input type="checkbox" name="remember-me">
            <input type="submit" value="submit">
        </form>
    </body>
    </html>
    
  6. 当我们启动成功后,发现数据库中已经自动创建了表:persistent_logins。当我们登录login.html页面,勾选Remember Me复选框,并且输入正确的用户名密码后,我们登录到了main.html页面。此时可以看到数据库中已经有了用户的数据。
    在这里插入图片描述
    当我们关闭浏览器,重新打开后,输入http://localhost:8080/main.html,此时,直接就会跳转到main.html页面,没有进行登录拦截。也就是“记住我”功能实现。
  7. 默认情况下,Token的失效时间时两周,我们可以自定义token实现时间。
    只需要将原来的代码改为:
    http.rememberMe()
            .tokenValiditySeconds(60)  // token 60s后失效,默认是两周时间
            //.rememberMeParameter()  // 这里是自定义表单上的 记住我的参数,默认是remember-me。
            .userDetailsService(userDetailsService)
            .tokenRepository(persistentTokenRepository);
    
12. Spring Security在thymeleaf中的使用

thymleaf 是spring官方推荐展示层,通常用在前后端不分离的项目中。Spring Security 在 thymeleaf中的使用,

  1. 引入thymeleaf 和 springsecurity 相关依赖
     <dependency>
          <groupId>org.thymeleaf.extras</groupId>
          <artifactId>thymeleaf-extras-springsecurity5</artifactId>
      </dependency>
    
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-thymeleaf</artifactId>
      </dependency>
    
  2. 在html页面中引入thymeleaf命名空间和 security命名空间
    <!DOCTYPE html>
    <!-- 要使用thymeleaf和springsecurity,相应的命名空间必须添加-->
    <html xmlns="http://www.w3.org/1999/xhtml"
     		xmlns:th="http://www.thymeleaf.org"
    		xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
    >
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
    	<span sec:authentication="name">123</span>
    	<span sec:authentication="principal.username">456</span>
    	<span sec:authentication="credentials">456</span>
    	<span sec:authentication="authorities">456</span>
    	<span sec:authentication="details.remoteAddress">456</span>
    	<span sec:authentication="details.sessionId">456</span>
    </body>
    </html>
    
  3. 使用thymeleaf,需要controller配合,实现页面跳转。
    @RequestMapping("demo")
    public String toDemo() {
        return "demo";  // 条状到demo.html页面
    }
    
  4. 上面在thymeleaf页面中,就可以获取到SpringSecurity用户相关的数据,而且,我们在thymeleaf页面,还可以进行一些权限的判断。
    修改自定义的UserDetailsServiceImpl类
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            System.out.println("exec loadUserByUsername");
            if (!"admin".equals(username)){
                throw  new UsernameNotFoundException("username is not found");
            }
            String password = passwordEncoder.encode("123");
            return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList(
                    "admin,normal,/insert,ROLE_abc"  // 增加了 /insert权限 和 abc的角色,角色之前需要有ROLE_前缀
            ));
        }
    
    }
    
    在demo.html页面增加如下代码:
    <br>
    通过权限判断:
    <button sec:authorize="hasAuthority('/insert')">add</button>
    
    
    通过角色判断:
    <button sec:authorize="hasRole('abc')">add</button>
    
13. 退出登录功能

我们需要在 configure中添加如下代码,即可实现登录后的退出功能。

  1. 在main.html页面中增加

    <a href="/logout">exit</a> 
    

    但是这样登出后我们会回到login.html页面,但是浏览器地址栏会显示http://localhost:8080/login.html?logout。如果我们不想要?logout这一串,我们需要在configure方法中配置。

  2. configure方法

       http.logout()
               .logoutUrl("/logout")   // 自定义登出的url,默认是/logout,可以自己定义
               .logoutSuccessUrl("/login.html"); // 登出成功后返回的页面
    

通过查看登出的源码,我们大概能够了解到,SpringSecurity帮我们做了两件事:让HttpSession实现和清除认证状态。

14. CSRF

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack”或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
客户端与服务器进行交互时,由于http协议本身的无状态性,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户身份。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务器发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。

Spring Security中的CSRF

从Spring Security4开始CSRF防护默认开启。默认情况下会拦截所有请求,进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csfr值为token的内容。如果客户端携带的token与服务器端的token匹配成功,则正常访问。

在我们前面的代码中,默认是增加了一行代码:

http.csrf().disable();

关闭了跨请求伪造功能。

开启跨站请求伪造功能,将该行代码注释掉。并且我们之前在 protected void configure(HttpSecurity http) 方法中,配置了直接跳转到登录页面:.loginPage("/login.html")。现在我们修改成通过controller跳转:

现在的 protected void configure(HttpSecurity http) 代码:

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .usernameParameter("username123")
                .passwordParameter("password123")
                .loginProcessingUrl("/login")
                .loginPage("/showLogin")   // 跳转到controller而不是原来的login.html
                        //.successForwardUrl("/toMain")
                //.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
                .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
                        //.failureForwardUrl("/toError");
                .failureHandler(new MyAuthenticationFailureHandler("/error.html"));

        http.authorizeRequests()
                //.antMatchers("/error.html").permitAll()
                .antMatchers("/error.html").access("permitAll()")
                //.antMatchers("/login.html").permitAll()
                .antMatchers("/showLogin").access("permitAll()")  // 放行/showLogin请求
                // .antMatchers("/main1.html").hasAuthority("abc")
                .antMatchers("/main1.html").access("hasAuthority('abc')")
                .anyRequest().authenticated();

        //http.csrf().disable();

        http.exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);

        http.rememberMe()
                .tokenValiditySeconds(60)
                //.rememberMeParameter()
                .userDetailsService(userDetailsService)
                .tokenRepository(persistentTokenRepository);

        http.logout()
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login.html");
    }

增加controller 的showLogin方法

@RequestMapping("showLogin")
public String showLogin() {
    return "login";
}

在引入thmeleaf的情况下,这个controller返回的是视图的名字,spring会找template目录下的login.html视图。
这个视图修改如下:

<!DOCTYPE html>
<!-- 引入了thmeleaf命名空间-->
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <form action="/login" method="post">
	    <!-- 开启csrf情况下,我们登录的时候,需要携带参数名_csrf值为token的一个参数,否则,即使用户名密码正确,也不能够登录 -->
        <input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"> 
        Username:<input type="text" name="username123"/><br/>
        Password: <input type="password" name="password123"/><br/>
        Remember Me: <input type="checkbox" name="remember-me">
        <input type="submit" value="submit">
    </form>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值