SpringBoot 集成 SpringSecurity完全解讀

一、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();            
      }
}
@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”;

  }

 html頁面是使用 thymeleaf 模板引擎的,這裏就不詳細講解了。

 

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>  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>
<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";
      }
}
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");
        }
        public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
                auth
                        .inMemoryAuthentication()
                                .withUser("user").password("password").roles("USER");
        }

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

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

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

      }

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

 @Override
      protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            // TODO Auto-generated method stub

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

            auth
            .inMemoryAuthentication()
                  .withUser("admin").password("123456").roles("USER")
                  .and()
                  .withUser("test").password("test123").roles("ADMIN");
      }

程序運行起來,這時用我們自己的用戶名和密碼 輸入 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接口

代碼如下:

public 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;
      }
} 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 null;
      }
}
            // TODO Auto-generated method stub


            //這裏可以通過數據庫來查找到實際的用戶信息,這裏我們先模擬下,後續我們用數據庫來實現
            if(username.equals("admin"))
            {
                  //假設返回的用戶信息如下;
                  UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);
                  return userInfo;

            }

            return null;
public class MyUserDetailsService implements UserDetailsService {



      @Override
      public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // TODO Auto-generated method stub
            //這裏可以可以通過username(登錄時輸入的用戶名)然後到數據庫中找到對應的用戶信息,並構建成我們自己的UserInfo來返回。
            return null;
      }
}
            // TODO Auto-generated method stub


            //這裏可以通過數據庫來查找到實際的用戶信息,這裏我們先模擬下,後續我們用數據庫來實現
            if(username.equals("admin"))
            {
                  //假設返回的用戶信息如下;
                  UserInfo userInfo=new UserInfo("admin", "123456", "ROLE_ADMIN", true,true,true, true);
                  return userInfo;

            }

            return null;

View Code

到這裏爲止,我們自己定義的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.getPrincipal();// 這個是表單中輸入的密碼;
            // 這裏構建來判斷用戶是否存在和密碼是否正確
            UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這裏調用我們的自己寫的獲取用戶的方法;
            if (userInfo == null) {
                  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;
      }
}
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.getPrincipal();// 這個是表單中輸入的密碼;
            // 這裏構建來判斷用戶是否存在和密碼是否正確
            UserInfo userInfo = (UserInfo) userDetailService.loadUserByUsername(userName); // 這裏調用我們的自己寫的獲取用戶的方法;
            if (userInfo == null) {
                  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;
      }
}

View Code

到此爲止,我們的用戶信息的獲取,校驗部分已經完成了。接下來要讓它起作用,則我們需要在配置文件中修改,讓他起作用。回到我的SecurityConfig代碼文件,修改如下:

1、注入我們自己的AuthenticationProvider

2、修改配置的方法:

    @Autowired
    private AuthenticationProvider provider;  //注入我們自己的AuthenticationProvider


    @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");
    }@Autowired
    private AuthenticationProvider provider;  //注入我們自己的AuthenticationProvider


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

View Code

現在重新運行程序,則需要輸入用戶名爲 admin 密碼是123456之後,才能正常登錄了。

爲了方便測試,我們調整添加另一個控制器 /whoim 的代碼 ,讓他返回當前登錄的用戶信息,前面說了,他是存在SecurityContextHolder 的全局變量中,所以我們可以這樣獲取

 
      @RequestMapping("/whoim")
      public Object whoIm()
      {
            return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      }@RequestMapping("/whoim")
      public Object whoIm()
      {
            return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      }

View Code

我們運行,直接反問 /whoim ,則直接跳轉到登錄頁面,我們驗證過之後,再訪問此url,結果如下:

到這裏,我們自定義的登錄已經成功了。

改造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, "/whoim");

      }
}
@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, "/whoim");

      }
}

View Code

//登錄失敗的
@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));

      }
}
@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));

      }
}

View Code

代碼完成之後,修改配置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();            
      }@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();            
      }

View Code

進行測試,我們先返回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();            
      }
      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();            
      }

View Code

那麼我們直接訪問 /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();   .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();   

View Code

這個用戶的角色哪裏來,就是我們自己的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’)”);這種。

 

那我們可以自己寫權限表達式嗎? 可以,稍後。。。這些都是硬編碼的實現,都是在代碼中寫入的,這樣的靈活性不夠。所以我們接下來繼續改造

改造4、添加基於RBAC(role-Based-access control)權限控制

這個大家可以去百度一下,一般都是由 3個部分組成,一個是用戶,一個是角色 ,一個是資源(菜單,按鈕),然後就是 用戶和角色的關聯表,角色和資源的關聯表

 

核心就是判斷當前的用戶所擁有的URL是否和當前訪問的URL是否匹配。

首先我們自己提供一個判斷的接口和實現,代碼如下:

/**
 * 返回權限驗證的接口
 * 
 *
 */
public interface RbacService {
      boolean hasPermission(HttpServletRequest request,Authentication authentication);
}

@Component("rbacService")
public class RbacServiceImpl implements RbacService {
      private AntPathMatcher antPathMatcher = new AntPathMatcher();
      @Override
      public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            Object principal = authentication.getPrincipal();
            boolean hasPermission = false;
            if (principal instanceof UserDetails) { //首先判斷先當前用戶是否是我們UserDetails對象。
                  String userName = ((UserDetails) principal).getUsername();
                  Set<String> urls = new HashSet<>(); // 數據庫讀取 //讀取用戶所擁有權限的所有URL

                  urls.add("/whoim");
                  // 注意這裏不能用equal來判斷,因爲有些URL是有參數的,所以要用AntPathMatcher來比較
                  for (String url : urls) {
                        if (antPathMatcher.match(url, request.getRequestURI())) {
                              hasPermission = true;
                              break;
                        }
                  }
            }
            return hasPermission;
      }
}
public interface RbacService {
      boolean hasPermission(HttpServletRequest request,Authentication authentication);
}

@Component("rbacService")
public class RbacServiceImpl implements RbacService {
      private AntPathMatcher antPathMatcher = new AntPathMatcher();
      @Override
      public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
            Object principal = authentication.getPrincipal();
            boolean hasPermission = false;
            if (principal instanceof UserDetails) { //首先判斷先當前用戶是否是我們UserDetails對象。
                  String userName = ((UserDetails) principal).getUsername();
                  Set<String> urls = new HashSet<>(); // 數據庫讀取 //讀取用戶所擁有權限的所有URL

                  urls.add("/whoim");
                  // 注意這裏不能用equal來判斷,因爲有些URL是有參數的,所以要用AntPathMatcher來比較
                  for (String url : urls) {
                        if (antPathMatcher.match(url, request.getRequestURI())) {
                              hasPermission = true;
                              break;
                        }
                  }
            }
            return hasPermission;
      }
}

View Code

然後在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()
                  .authorizeRequests()
//                      .antMatchers("/index").permitAll()                    
//                .antMatchers("/whoim").hasRole("ADMIN")
//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必須經過認證以後才能訪問            
                  .and()
                  .csrf().disable();            
      }
      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()                    
//                .antMatchers("/whoim").hasRole("ADMIN")
//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必須經過認證以後才能訪問            
                  .and()
                  .csrf().disable();            
      }

View Code

其中 @rbacService 就是我們自己聲明的bean,在RbacServiceImpl實現類的頭部註解中。

改造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)
);(
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) NOT NULL,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL,
    PRIMARY KEY (series)
);

然後,配置好token 的存儲 及數據源

 @Autowired
      private DataSource dataSource;   //是在application.properites

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

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

View Code

修改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(userDetailsService)
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(60)
                  .and()
                  .authorizeRequests()
//                      .antMatchers("/index").permitAll()                    
//                .antMatchers("/whoim").hasRole("ADMIN")
//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必須經過認證以後才能訪問            
                  .and()
                  .csrf().disable();      @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(userDetailsService)
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(60)
                  .and()
                  .authorizeRequests()
//                      .antMatchers("/index").permitAll()                    
//                .antMatchers("/whoim").hasRole("ADMIN")
//                .antMatchers(HttpMethod.POST,"/user/*").hasRole("ADMIN")
//                .antMatchers(HttpMethod.GET,"/user/*").hasRole("USER")
                  .anyRequest().access("@rbacService.hasPermission(request,authentication)")    //必須經過認證以後才能訪問            
                  .and()
                  .csrf().disable();      

View Code

登錄之後 數據庫就會有一條數據

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章