使用SpringSecurity保護程序安全

首先,引入依賴:html

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

引入此依賴以後,你的web程序將擁有如下功能:java

  • 全部請求路徑都須要認證
  • 不須要特定的角色和權限
  • 沒有登陸頁面,使用HTTP基自己份認證
  • 只有一個用戶,名稱爲user


配置SpringSecurity

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

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
}



配置用戶認證方式spring

首先,要解決的就是用戶註冊,保存用戶的信息。springsecurity提供四種存儲用戶的方式:數據庫

  • 基於內存(生產確定不使用)
  • 基於JDBC
  • 基於LDAP
  • 用戶自定義(最經常使用)

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

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

1.基於內存session

@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.基於JDBCapp

@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";

固然,你能夠對這些語句進行一下修改:ide

@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:/";
}
相關文章
相關標籤/搜索