1. 程式人生 > >【轉】springboot整合spring security(一)

【轉】springboot整合spring security(一)

原文作者:王文健 
來源:CSDN 
轉自原文:https://blog.csdn.net/qq_29580525/article/details/79317969 

但是原文有幾處錯誤,且本文也結合了其他自己的知識,可以說是上文的升級版本

一、Spring security 是什麼?

Spring Security是一個能夠為基於Spring的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。

它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面程式設計)功能,為應用系統提供宣告式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重複程式碼的工作。

二、Spring security 怎麼使用?

使用Spring Security很簡單,只要在pom.xml檔案中,引入spring security的依賴就可以了。           

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


什麼都不做,直接執行程式,這時你訪問任何一個URL,都會彈出一個“需要授權”的驗證框,如圖:

,spring security 會預設使用一個使用者名稱為:user 的使用者,密碼就是 啟動的時候生成的(通過控制檯console中檢視),如圖

然後在使用者名稱中輸入:user   密碼框中輸入 上面的密碼 ,之後就可以正常訪問之前URL了。很顯然這根本不是我們想要的,接下來我們需要一步一步的改造。

 改造1 使用頁面表單登入

通過修改Security的配置來實現  參考:https://docs.spring.io/spring-security/site/docs/current/guides/html5//helloworld-boot.html#creating-your-spring-security-configuration

首先 新增一個類 SecurityConfig 繼承 WebSecurityConfigurerAdapter ,

重寫configure方法。

並加上@Configuration 和@EnableWebSecurity 2個註解。

//這是安全控制中心
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
      
      @Override
      protected void configure(HttpSecurity http) throws Exception {
            // TODO Auto-generated method stub
            //super.configure(http);
            http
                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form").failureUrl("/login-error").permitAll()  //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
                  .and()
                  .authorizeRequests().anyRequest().authenticated()                  
                  .and()
                  .csrf().disable();            
      }
}

View Code

 loginPage("/login")表示登入時跳轉的頁面,因為登入頁面我們不需要登入認證,所以我們需要新增 permitAll() 方法。

新增一個控制器,對應/login 返回一個登入頁面。

@RequestMapping("/login")
  public String userLogin(){
 
        return "demo-sign";
  }

demo_sign.html 的 html部分程式碼如下:  

<form  class="form-signin" action="/login/form" method="post">
              <h2 class="form-signin-heading">使用者登入</h2>
            <table>
                  <tr>
                        <td>使用者名稱:</td>
                        <td><input type="text" name="username"  class="form-control"  placeholder="請輸入使用者名稱"/></td>
                  </tr>
                        <tr>
                        <td>密碼:</td>
                        <td><input type="password" name="password"  class="form-control" placeholder="請輸入密碼" /></td>
                  </tr>
                  <tr>
                  
                        <td colspan="2">
                              <button type="submit"  class="btn btn-lg btn-primary btn-block" >登入</button>
                        </td>
                  </tr>
            </table>
      </form>

需要注意下:form提交的url要和配置檔案中的 loginProcessingUrl("")中的一致。

failureUrl=表示登入出錯的頁面,我們可以簡單寫個提示:如 使用者名稱或密碼錯誤。

@RequestMapping("/login-error")
  public String loginError(){
        return "login-error";
  }

login-error.html

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
<title>使用者登入</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" />
<link rel="stylesheet" href="/css/sign.css" />
</head>
<body>
            <h3>使用者名稱或密碼錯誤</h3>
</body>
</html>

 執行程式:如果輸入錯誤的使用者名稱和密碼的話,則會顯示如下圖所示:

我們用一個測試的RestController來測試

@RestController
public class HelloWorldController {
      @RequestMapping("/hello")
      public String helloWorld(){
            return "spring security hello world";
      }
}

當沒有登入時,輸入 http://localhost:port/hello 時,則直接跳轉到我們登入頁面,登入成功之後,再訪問 時,就能顯示我們期望的值了。

改造2、自定義使用者名稱和密碼

很顯然,這樣改造之後,雖然登入頁面是好看了,但還遠遠不能滿足我們的應用需求,所以第二步,我們改造自定義的使用者名稱和密碼。

自定義使用者名稱和密碼有2種方式,一種是在程式碼中寫死,這也是官方的demo,另一種是使用資料庫

首先是第一種:如 

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
                auth.inMemoryAuthentication()
                              .withUser("user").password("password").roles("USER");
}

我們也照樣,這是把使用者名稱改成 admin 密碼改成 123456   roles是該使用者的角色,我們後面再細說。

還有種方法 就是 重寫 另外一種configure(AuthenticationManagerBuilder auth) 方法,這個和上面那個方法的作用是一樣的。選其一就可。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
            
            auth
            .inMemoryAuthentication()
                  .withUser("admin").password("123456").roles("USER")
                  .and()
                  .withUser("test").password("test123").roles("ADMIN");
}

注意,到這一步,舊版本的springsecurity就已經可以運行了,但是新版本下,必須要做進一步操作,

這是因為Spring boot 2.0.3引用的security 依賴是 spring security 5.X版本,此版本需要提供一個PasswordEncorder的例項,否則後臺彙報錯誤:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
並且頁面毫無響應。
因此,需要建立PasswordEncorder的實現類。
public class MyPasswordEncoder implements PasswordEncoder {
    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

在安全控制中心SecurityConfig下配置改為:

@Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                ////不加.passwordEncoder(new MyPasswordEncoder())
                //        //就不是以明文的方式進行匹配,會報錯
                //加的話在頁面提交時候,密碼會以明文的方式進行匹配
                .passwordEncoder(new MyPasswordEncoder())
                //這是把使用者名稱改成 admin 密碼改成 123456   roles是該使用者的角色,我們後面再細說。
                .withUser("user").password("password").roles("USER")
                .and()
                .withUser("user1").password("password").roles("USER");//可以多寫幾個User
}

程式執行起來,這時用我們自己的使用者名稱和密碼 輸入 admin 和123456 就可以了

你也可以多幾個使用者,就多幾個withUser即可。

.and().withUser("test").password("test123").roles("ADMIN");  這樣我們就有了一個使用者名稱為test,密碼為test123的使用者了。

第一種的只是讓我們體驗了一下Spring Security而已,我們接下來就要提供自定義的使用者認證機制及處理過程。

在講這個之前,我們需要知道spring security的原理,spring security的原理就是使用很多的攔截器對URL進行攔截,以此來管理登入驗證和使用者許可權驗證。

 

使用者登陸,會被AuthenticationProcessingFilter攔截,呼叫AuthenticationManager的實現,而且AuthenticationManager會呼叫ProviderManager來獲取使用者驗證資訊(不同的Provider呼叫的服務不同,因為這些資訊可以是在資料庫上,可以是在LDAP伺服器上,可以是xml配置檔案上等),如果驗證通過後會將使用者的許可權資訊封裝一個User放到spring的全域性快取SecurityContextHolder中,以備後面訪問資源時使用。

 

所以我們要自定義使用者的校驗機制的話,我們只要實現自己的AuthenticationProvider就可以了。在用AuthenticationProvider 這個之前,我們需要提供一個獲取使用者資訊的服務,實現  UserDetailsService 介面

使用者名稱密碼->(Authentication(未認證)  ->  AuthenticationManager ->AuthenticationProvider->UserDetailService->UserDetails->Authentication(已認證)

瞭解了這個原理之後,我們就開始寫程式碼

第一步:我們定義自己的使用者資訊類 UserInfo 繼承UserDetails和Serializable介面

程式碼如下:
 

class UserInfo implements Serializable, UserDetails {
      /**
       *
       */
      private static final long serialVersionUID = 1L;
      private String username;
      private String password;
      private String role;
      private boolean accountNonExpired;
      private boolean accountNonLocked;
      private boolean credentialsNonExpired;
      private boolean enabled;
      public UserInfo(String username, String password, String role, boolean accountNonExpired, boolean accountNonLocked,
                  boolean credentialsNonExpired, boolean enabled) {
            // TODO Auto-generated constructor stub
            this.username = username;
            this.password = password;
            this.role = role;
            this.accountNonExpired = accountNonExpired;
            this.accountNonLocked = accountNonLocked;
            this.credentialsNonExpired = credentialsNonExpired;
            this.enabled = enabled;
      }
      // 這是許可權
      @Override
      public Collection<? extends GrantedAuthority> getAuthorities() {
            // TODO Auto-generated method stub
            return AuthorityUtils.commaSeparatedStringToAuthorityList(role);
      }
      @Override
      public String getPassword() {
            // TODO Auto-generated method stub
            return password;
      }
      @Override
      public String getUsername() {
            // TODO Auto-generated method stub
            return username;
      }
      @Override
      public boolean isAccountNonExpired() {
            // TODO Auto-generated method stub
            return accountNonExpired;
      }
      @Override
      public boolean isAccountNonLocked() {
            // TODO Auto-generated method stub
            return accountNonLocked;
      }
      @Override
      public boolean isCredentialsNonExpired() {
            // TODO Auto-generated method stub
            return credentialsNonExpired;
      }
      @Override
      public boolean isEnabled() {
            // TODO Auto-generated method stub
            return enabled;
      }
}

View Code

然後實現第2個類 UserService 來返回這個UserInfo的物件例項

@Component
public class MyUserDetailsService implements UserDetailsService {
        
      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // TODO Auto-generated method stub
            //這裡可以可以通過username(登入時輸入的使用者名稱)然後到資料庫中找到對應的使用者資訊,並構建成我們自己的UserInfo來返回。
            return ;
      }
}
            // TODO Auto-generated method stub
            
            
            //這裡可以通過資料庫來查詢到實際的使用者資訊,這裡我們先模擬下,後續我們用資料庫來實現
            if(username.equals("admin"))
            {
                  //假設返回的使用者資訊如下;
                  UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);
                  return userInfo;
                              
            }
            
            return ;
 
}

到這裡為止,我們自己定義的UserInfo類和從資料庫中返回具體的使用者資訊已經實現,接下來我們要實現的,我們自己的 AuthenticationProvider

新建類 MyAuthenticationProvider 繼承AuthenticationProvider

完整的程式碼如下:

@Component
public class MyAuthenticationProvider implements AuthenticationProvider {
      /**
       * 注入我們自己定義的使用者資訊獲取物件
       */
      @Autowired
      private UserDetailsService userDetailService;
      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            // TODO Auto-generated method stub
            String userName = authentication.getName();// 這個獲取表單輸入中返回的使用者名稱;
            String password = (String) authentication.getCredentials();// 這個是表單中輸入的密碼;
            // 這裡構建來判斷使用者是否存在和密碼是否正確
            UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這裡呼叫我們的自己寫的獲取使用者的方法;
            if (userInfo == ) {
                  throw new BadCredentialsException("使用者名稱不存在");
            }
            // //這裡我們還要判斷密碼是否正確,實際應用中,我們的密碼一般都會加密,以Md5加密為例
            // Md5PasswordEncoder md5PasswordEncoder=new Md5PasswordEncoder();
            // //這裡第個引數,是salt
            // 就是加點鹽的意思,這樣的好處就是使用者的密碼如果都是123456,由於鹽的不同,密碼也是不一樣的,就不用怕相同密碼洩漏之後,不會批量被破解。
            // String encodePwd=md5PasswordEncoder.encodePassword(password, userName);
            // //這裡判斷密碼正確與否
            // if(!userInfo.getPassword().equals(encodePwd))
            // {
            // throw new BadCredentialsException("密碼不正確");
            // }
            // //這裡還可以加一些其他資訊的判斷,比如使用者賬號已停用等判斷,這裡為了方便我接下去的判斷,我就不用加密了。
            //
            //
            if (!userInfo.getPassword().equals("123456")) {
                  throw new BadCredentialsException("密碼不正確");
            }
            Collection<? extends GrantedAuthority> authorities = userInfo.getAuthorities();
            // 構建返回的使用者登入成功的token
            return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
      }
      @Override
      public boolean supports(Class<?> authentication) {
            // TODO Auto-generated method stub
            // 這裡直接改成retrun true;表示是支援這個執行
            return true;
      }
}
到此為止,我們的使用者資訊的獲取,校驗部分已經完成了。接下來要讓它起作用,則我們需要在配置檔案中修改,讓他起作用。回到我的SecurityConfig程式碼檔案,修改如下:

1、注入我們自己的AuthenticationProvider

2、修改配置的方法:

@Autowired
    private MyAuthenticationProvider myAuthenticationProvider;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // TODO Auto-generated method stub
        auth.authenticationProvider(provider);
 
 
//        auth
//        .inMemoryAuthentication()
//            .withUser("admin").password("123456").roles("USER")
//            .and()
//            .withUser("test").password("test123").roles("ADMIN");
    }

現在重新執行程式,則需要輸入使用者名稱為 admin 密碼是123456之後,才能正常登入了。

改造3、自定義登入成功和失敗的處理邏輯

在現在的大多數應用中,一般都是前後端分離的,所以我們登入成功或失敗都需要用json格式返回,或者登入成功之後,跳轉到某個具體的頁面。

接下來我們來實現這種改造。

為了實現這個功能,我們需要寫2個類,分別繼承SavedRequestAwareAuthenticationSuccessHandler和SimpleUrlAuthenticationFailureHandler2個類,並重寫其中的部分方法即可。

@Component("myAuthenticationSuccessHandler")
////處理登入成功的。
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        //什麼都不做的話,那就直接呼叫父類的方法
        /*super.onAuthenticationSuccess(request, response, authentication);*/

        //這裡可以根據實際情況,來確定是跳轉到頁面或者json格式。
        //如果是返回json格式,那麼我們這麼寫

        /*Map<String,String> map=new HashMap<>();
        map.put("code", "200");
        map.put("msg", "登入成功");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(map));
*/

        //如果是要跳轉到某個頁面的,比如我們的那個whoim的則
        new DefaultRedirectStrategy().sendRedirect(request, response, "/login-success");
        return;
    }
}
//登入失敗的
@Component("myAuthenticationFailHander")
public class MyAuthenticationFailHander extends SimpleUrlAuthenticationFailureHandler {
      @Autowired
      private ObjectMapper objectMapper;
      private Logger logger = LoggerFactory.getLogger(getClass());
      @Override
      public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                  AuthenticationException exception) throws IOException, ServletException {
            // TODO Auto-generated method stub
            logger.info("登入失敗");
            //以Json格式返回
            Map<String,String> map=new HashMap<>();
            map.put("code", "201");
            map.put("msg", "登入失敗");
            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json");
            response.setCharacterEncoding("UTF-8");   
            response.getWriter().write(objectMapper.writeValueAsString(map));
            
      }
}

程式碼完成之後,修改配置config類程式碼。

新增2個註解,自動注入

@Autowired
      private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
      @Autowired
      private AuthenticationFailureHandler myAuthenticationFailHander;
      
      @Override
      protected void configure(HttpSecurity http) throws Exception {
            // TODO Auto-generated method stub
            //super.configure(http);
            http
                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")
                  .successHandler(myAuthenticationSuccessHandler)
                  .failureHandler(myAuthenticationFailHander)
                  .permitAll()  //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
                  .and()
                  .authorizeRequests().anyRequest().authenticated()                  
                  .and()
                  .csrf().disable();            
      }

進行測試,我們先返回json格式的(登入成功和失敗的)

改成跳轉到預設頁面

改造4、新增許可權控制

之前的程式碼我們使用者的許可權沒有加以利用,現在我們新增許可權的用法。

之前的登入驗證通俗的說,就是來判斷你是誰(認證),而許可權控制就是用來確定:你能做什麼或者不能做什麼(許可權)

 

在講這個之前,我們簡單說下,對於一些資源不需要許可權認證的,那麼就可以在Config中新增 過濾條件,如:
 

@Override
      protected void configure(HttpSecurity http) throws Exception {
            // TODO Auto-generated method stub
            //super.configure(http);
            http
                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")
                  .successHandler(myAuthenticationSuccessHandler)
                  .failureHandler(myAuthenticationFailHander)
                  .permitAll()  //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
                  .and()
                  .authorizeRequests()
                        .antMatchers("/index").permitAll()  //這就表示 /index這個頁面不需要許可權認證,所有人都可以訪問
                  .anyRequest().authenticated()             
                  .and()
                  .csrf().disable();            
      }

那麼我們直接訪問 /index 就不會跳轉到登入頁面,這樣我們就可以把一些不需要驗證的資源以這種方式過濾,比如圖片,指令碼,樣式檔案之類的。

我們先來看第一種:在編碼中寫死的。

那其實許可權控制也是通過這種方式來實現:

http
                  .formLogin().loginPage("/login").loginProcessingUrl("/login/form")
                  .successHandler(myAuthenticationSuccessHandler)
                  .failureHandler(myAuthenticationFailHander)
                  .permitAll()  //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
                  .and()
                  .authorizeRequests()
                        .antMatchers("/index").permitAll()                    
                  .antMatchers("/whoim").hasRole("ADMIN") //這就表示/whoim的這個資源需要有ROLE_ADMIN的這個角色才能訪問。不然就會提示拒絕訪問
                  .anyRequest().authenticated() //必須經過認證以後才能訪問          
                  .and()
                  .csrf().disable();

這個使用者的角色哪裡來,就是我們自己的UserDetailsService中返回的使用者資訊中的角色許可權資訊,這裡需要注意一下就是 .hasRole("ADMIN"),那麼給使用者的角色時就要用:ROLE_ADMIN 

.antMatchers 這裡也可以限定HttpMethod的不同要求不同的許可權(用於適用於Restful風格的API).

如:Post需要 管理員許可權,get 需要user許可權,我們可以這麼個改造,同時也可以通過萬用字元來是實現 如:/user/1 這種帶引數的URL

.antMatchers("/whoim").hasRole("ADMIN")
 
      .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
 
      .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")

Spring Security 的校驗的原理:左手配置資訊,右手登入後的使用者資訊,中間投票器。

 從我們的配置資訊中獲取相關的URL和需要的許可權資訊,然後獲得登入後的使用者資訊,

然後經過:AccessDecisionManager 來驗證,這裡面有多個投票器:AccessDecisionVoter,(預設有幾種實現:比如:1票否決(只要有一個不同意,就沒有許可權),全票通過,才算通過;只要有1個通過,就全部通過。類似這種的。

WebExpressionVoter 是Spring Security預設提供的的web開發的投票器。(表示式的投票器)

 

Spring Security 預設的是 AffirmativeBased   只要有一個通過,就通過。

有興趣的可以 從FilterSecurityInterceptor這個過濾器入口,來檢視這個流程。

內嵌的表示式有:permitAll  denyAll   等等。

每一個許可權表示式都對應一個方法。

如果需要同時滿足多個要求的,不能連寫如 ,我們有個URL需要管理員許可權也同時要限定IP的話,不能:.hasRole("ADMIN").hasIPAddress("192.168.1.1"); 

而是需要用access方法    .access("hasRole('ADMIN') and hasIpAddress('192.168.1.1')");這種。

 

那我們可以自己寫許可權表示式嗎? 可以,稍後。。。這些都是硬編碼的實現,都是在程式碼中寫入的,這樣的靈活性不夠。所以我們接下來繼續改造
改造5、記住我的功能Remeber me

本質是通過token來讀取使用者資訊,所以服務端需要儲存下token資訊

根據官方的文件,token可以通過資料庫儲存 資料庫指令碼

必須嚴格按照下面的格式來建立資料庫的表!!

CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) NOT NULL,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL,
    PRIMARY KEY (series)
);

然後,配置好token 的儲存 及資料來源  在SecurityConfig裡寫:

@Autowired
      private DataSource dataSource;   //是在application.properites
 
      /**
       * 記住我功能的token存取器配置
       * @return
       */
      @Bean
      public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            return tokenRepository;
      }

修改Security配置

@Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO Auto-generated method stub
        //super.configure(http);
        http
                .formLogin().loginPage("/login").loginProcessingUrl("/login/form")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailHander)
                .permitAll()  //表單登入,permitAll()表示這個不需要驗證 登入頁面,登入失敗頁面
                .and()
                .rememberMe()
                .rememberMeParameter("remember-me").userDetailsService(myUserDetailsService)
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(1209600)
                .and()
                .authorizeRequests()
                .antMatchers("/hello1").permitAll()//表示/index這個請求不需要許可權認證,所有人都可以訪問
                .antMatchers("/whoim").hasRole("ADMIN")//表示/whoim需要有ROLE_ADMIN許可權的人才可以訪問
                .anyRequest().authenticated()
                .and()
                .csrf().disable();// //取消放置csrf攻擊的防護
        /*關於上面的程式碼解釋:loginPage("/login")表示登入時跳轉的頁面,因為我們不需要登入認證,所以加一個.permitAll()方法
          failureUrl=表示登入出錯的頁面
        */
    }

登入之後 資料庫就會有一條資料

然後,服務重新啟動下,我們在看下直接訪問 /whoim 的話,就可以直接訪問了,不需要再登入了。

到此為止我們的Spring Securtiy 的基本用法已經改造完成了。