使用SpringSecurity保護程式安全

  • 2019 年 10 月 3 日
  • 筆記

首先,引入依賴:

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

引入此依賴之後,你的web程式將擁有以下功能:

  • 所有請求路徑都需要認證
  • 不需要特定的角色和許可權
  • 沒有登錄頁面,使用HTTP基本身份認證
  • 只有一個用戶,名稱為user

配置SpringSecurity

springsecurity配置項,最好保存在一個單獨的配置類中:

@Configuration  @EnableWebSecurity  public class SecurityConfig extends WebSecurityConfigurerAdapter {    }

配置用戶認證方式

首先,要解決的就是用戶註冊,保存用戶的資訊。springsecurity提供四種存儲用戶的方式:

  • 基於記憶體(生產肯定不使用)
  • 基於JDBC
  • 基於LDAP
  • 用戶自定義(最常用)

使用其中任意一種方式,需要覆蓋configure(AuthenticationManagerBuilder auth)方法:

@Configuration  @EnableWebSecurity  public class SecurityConfig extends WebSecurityConfigurerAdapter {      @Override      protected void configure(AuthenticationManagerBuilder auth) throws Exception {      }  }

1.基於記憶體

@Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {      auth.inMemoryAuthentication()              .withUser("zhangsan").password("123").authorities("ROLE_USER")              .and()              .withUser("lisi").password("456").authorities("ROLE_USER");  }

2.基於JDBC

@Autowired  DataSource dataSource;    @Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {      auth.jdbcAuthentication()          .dataSource(dataSource);  }

基於JDBC的方式,你必須有一些特定表表,而且欄位滿足其查詢規則:

public static final String DEF_USERS_BY_USERNAME_QUERY =      "select username,password,enabled " +      "from users " +      "where username = ?";  public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY =      "select username,authority " +      "from authorities " +      "where username = ?";  public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY =      "select g.id, g.group_name, ga.authority " +      "from groups g, group_members gm, group_authorities ga " +      "where gm.username = ? " +      "and g.id = ga.group_id " +      "and g.id = gm.group_id";

當然,你可以對這些語句進行一下修改:

@Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {      auth.jdbcAuthentication().dataSource(dataSource)          .usersByUsernameQuery("select username, password, enabled from Users " +                                "where username=?")          .authoritiesByUsernameQuery("select username, authority from UserAuthorities " +                                      "where username=?");

這有一個問題,你資料庫中的密碼可能是一種加密方式加密過的,而用戶傳遞的是明文,比較的時候需要進行加密處理,springsecurity也提供了相應的功能:

@Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {      auth.jdbcAuthentication().dataSource(dataSource)          .usersByUsernameQuery("select username, password, enabled from Users " +                                "where username=?")          .authoritiesByUsernameQuery("select username, authority from UserAuthorities " +                                      "where username=?")          .passwordEncoder(new StandardPasswordEncoder("53cr3t");

passwordEncoder方法傳遞的是PasswordEncoder介面的實現,其默認提供了一些實現,如果都不滿足,你可以實現這個介面:

  • BCryptPasswordEncoder

  • NoOpPasswordEncoder
  • Pbkdf2PasswordEncoder
  • SCryptPasswordEncoder
  • StandardPasswordEncoder(SHA-256)

3.基於LDAP

@Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {      auth.ldapAuthentication()          .userSearchBase("ou=people")          .userSearchFilter("(uid={0})")          .groupSearchBase("ou=groups")          .groupSearchFilter("member={0}")          .passwordCompare()          .passwordEncoder(new BCryptPasswordEncoder())          .passwordAttribute("passcode")          .contextSource()              .root("dc=tacocloud,dc=com")              .ldif("classpath:users.ldif");

4.用戶自定義方式(最常用)

首先,你需要一個用戶實體類,它實現UserDetails介面,實現這個介面的目的是為框架提供更多的資訊,你可以把它看作框架使用的實體類:

@Data  public class User implements UserDetails {        private Long id;      private String username;      private String password;      private String fullname;      private String city;      private String phoneNumber;        @Override      public Collection<? extends GrantedAuthority> getAuthorities() {          return null;      }        @Override      public boolean isAccountNonExpired() {          return false;      }        @Override      public boolean isAccountNonLocked() {          return false;      }        @Override      public boolean isCredentialsNonExpired() {          return false;      }        @Override      public boolean isEnabled() {          return false;      }  }

有了實體類,你還需要Service邏輯層,springsecurity提供了UserDetailsService介面,見名知意,你只要通過loadUserByUsername返回一個UserDetails對象就成,無論是基於文件、基於資料庫、還是基於LDAP,剩下的對比判斷交個框架完成:

@Service  public class UserService implements UserDetailsService {        @Override      public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {          return null;      }    }

最後,進行應用:

@Autowired  private UserDetailsService userDetailsService;    @Bean  public PasswordEncoder encoder() {      return new StandardPasswordEncoder("53cr3t");  }    @Override  protected void configure(AuthenticationManagerBuilder auth) throws Exception {      auth.userDetailsService(userDetailsService)          .passwordEncoder(encoder());  }

配置認證路徑

知道了如何認證,但現在有幾個問題,比如,用戶登錄頁面就不需要認證,可以用configure(HttpSecurity http)對認證路徑進行配置:

@Override  protected void configure(HttpSecurity http) throws Exception {  }

你可以通過這個方法,實現以下功能:

  • 在提供介面服務前,判斷請求必須滿足某些條件
  • 配置登錄頁面
  • 允許用戶註銷登錄
  • 跨站點偽造請求防護

1.保護請求

@Override  protected void configure(HttpSecurity http) throws Exception {      http.authorizeRequests()          .antMatchers("/design", "/orders").hasRole("ROLE_USER")          .antMatchers(「/」, "/**").permitAll();  }

要注意其順序,除了hasRolepermitAll還有其它訪問認證方法:

方法 作用
access(String) 如果給定的SpEL表達式的計算結果為true,則允許訪問
anonymous() 允許訪問匿名用戶
authenticated() 允許訪問經過身份驗證的用戶
denyAll() 無條件拒絕訪問
fullyAuthenticated() 如果用戶完全通過身份驗證,則允許訪問
hasAnyAuthority(String…) 如果用戶具有任何給定許可權,則允許訪問
hasAnyRole(String…) 如果用戶具有任何給定角色,則允許訪問
hasAuthority(String) 如果用戶具有給定許可權,則允許訪問
hasIpAddress(String) 如果請求來自給定的IP地址,則允許訪問
hasRole(String) 如果用戶具有給定角色,則允許訪問
not() 否定任何其他訪問方法的影響
permitAll() 允許無條件訪問
rememberMe() 允許通過remember-me進行身份驗證的用戶訪問

大部分方法是為特定方式準備的,但是access(String)可以使用SpEL進一些特殊的設置,但其中很大一部分也和上面的方法相同:

表達式 作用
authentication 用戶的身份驗證對象
denyAll 始終評估為false
hasAnyRole(list of roles) 如果用戶具有任何給定角色,則為true
hasRole(role) 如果用戶具有給定角色,則為true
hasIpAddress(IP address) 如果請求來自給定的IP地址,則為true
isAnonymous() 如果用戶是匿名用戶,則為true
isAuthenticated() 如果用戶已通過身份驗證,則為true
isFullyAuthenticated() 如果用戶已完全通過身份驗證,則為true(未通過remember-me進行身份驗證)
isRememberMe() 如果用戶通過remember-me進行身份驗證,則為true
permitAll 始終評估為true
principal 用戶的主要對象

示例:

@Override  protected void configure(HttpSecurity http) throws Exception {      http.authorizeRequests()          .antMatchers("/design", "/orders").access("hasRole('ROLE_USER')")          .antMatchers(「/」, "/**").access("permitAll");  }
@Override  protected void configure(HttpSecurity http) throws Exception {      http.authorizeRequests()          .antMatchers("/design", "/orders").access("hasRole('ROLE_USER') && " +           "T(java.util.Calendar).getInstance().get("+"T(java.util.Calendar).DAY_OF_WEEK) == " +          "T(java.util.Calendar).TUESDAY")          .antMatchers(「/」, "/**").access("permitAll");  }

2.配置登錄頁面

@Override  protected void configure(HttpSecurity http) throws Exception {      http.authorizeRequests()          .antMatchers("/design", "/orders").access("hasRole('ROLE_USER')")          .antMatchers(「/」, "/**").access("permitAll")          .and()          .formLogin()              .loginPage("/login");  }    // 增加視圖處理器  @Overridepublic void addViewControllers(ViewControllerRegistry registry) {      registry.addViewController("/").setViewName("home");      registry.addViewController("/login");  }

默認情況下,希望傳遞的是usernamepassword,當然你可以修改:

.and()      .formLogin()          .loginPage("/login")          .loginProcessingUrl("/authenticate")          .usernameParameter("user")          .passwordParameter("pwd")

也可修改默認登錄成功的頁面:

.and()      .formLogin()          .loginPage("/login")          .defaultSuccessUrl("/design")

3.配置登出

.and()      .logout()           .logoutSuccessUrl("/")

4.csrf攻擊

springsecurity默認開啟了防止csrf攻擊,你只需要在傳遞的時候加上:

<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>

當然,你也可以關閉,但是不建議這樣做:

.and()      .csrf()          .disable()

知道用戶是誰

僅僅控制用戶登錄有時候是不夠的,你可能還想在程式的其它地方獲取已經登錄的用戶資訊,有幾種方式可以做到:

  • Principal對象注入控制器方法

  • Authentication對象注入控制器方法
  • 使用SecurityContextHolder獲取安全上下文
  • 使用@AuthenticationPrincipal註解方法

1.將Principal對象注入控制器方法

@PostMappingpublic String processOrder(@Valid Order order, Errors errors,SessionStatus sessionStatus,Principal principal) {      ...      User user = userRepository.findByUsername(principal.getName());      order.setUser(user);      ...  }

2.將Authentication對象注入控制器方法

@PostMappingpublic String processOrder(@Valid Order order, Errors errors, SessionStatus sessionStatus, Authentication authentication) {      ...      User user = (User) authentication.getPrincipal();      order.setUser(user);      ...  }

3.使用SecurityContextHolder獲取安全上下文

Authentication authentication =      SecurityContextHolder.getContext().getAuthentication();      User user = (User) authentication.getPrincipal();

4.使用@AuthenticationPrincipal註解方法

@PostMappingpublic String processOrder(@Valid Order order, Errors errors,SessionStatus sessionStatus, @AuthenticationPrincipal User user) {      if (errors.hasErrors()) {          return "orderForm";      }      order.setUser(user);      orderRepo.save(order);      sessionStatus.setComplete();      return "redirect:/";  }