spring boot 2.0.3+spring cloud (Finchley)九、 安全組件Spring Boot Security

 官方文檔css

1、Spring Security介紹html

Spring Security是Spring Resource社區的一個安全組件,Spring Security爲JavaEE企業級開發提供了全面的安全防禦。Spring Security採用「安全層」的概念,使每一層都儘量安全,連續的安全層能夠達到全面的防禦。Spring Security能夠在Controller層、Service層、DAO層等以加註解的方式來保護應用程序的安全。Spring Security提供了細粒度的權限控制,能夠精細到每個API接口、每個業務的方法,或每個操做數據庫的DAO層的方法。Spring Security提供的是應用程序層的安全解決方案,一個系統的安全還須要考慮傳輸層和系統層的安全,如採用Https協議、服務器部署防火牆等。java

使用Spring Security的一個重要緣由是它對環境的無依賴性、低代碼耦合性。Spring Security提供了數十個安全模塊,模塊與模塊之間的耦合性低,模塊之間能夠自由組合來實現特定需求的安全功能。mysql

在安全方面,有兩個主要的領域,一是「認證」,即你是誰;二是「受權」,即你擁有什麼權限,Spring Security的主要目標就是在這兩個領域。JavaEE有另外一個優秀的安全框架Apache Shiro,Apache Shiro在企業及的項目開發中十分受歡迎,通常使用在單體服務中。但在微服務架構中,目前版本的Apache Shiro是無能爲力的。另外一個選擇Spring Security的緣由,是Spring Security易應用於Spring Boot工程,也易於集成到採用Spring Cloud構建的微服務系統中。git

Spring Security提供了不少的安全驗證模塊並支持與不少技術的整合,在Spring Security框架中,主要包含了兩個依賴,分別是spring-security-web依賴和spring-security-config依賴。Spring Boot對Spring Security框架作了封閉,僅僅是封閉,並無改動,並加上了Spring Boot的啓動依賴特性。使用時只須要引入spring-boot-starter-security。github

 2、使用案例web

新建一個Spring Boot工程,引入相關依賴算法

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>

配置Spring Securityspring

新建WebSecurityConfig類,做爲配置類繼承了WebSecurityConfigurerAdapter類,加上@EnableWebSecurity註解,開啓WebSecurity功能。sql

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
    }
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cralor").password(new BCryptPasswordEncoder().encode("123")).roles("USER").build());
        manager.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("123")).roles("ADMIN").build());
        return manager;
    }

使用Spring Security須要對密碼加密,這裏使用BCryptPasswordEncoder。

spring security中的BCryptPasswordEncoder方法採用SHA-256 +隨機鹽+密鑰對密碼進行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着能夠解密(這個與編碼/解碼同樣),可是採用Hash處理,其過程是不可逆的。

(1)加密(encode):註冊用戶時,使用SHA-256+隨機鹽+密鑰把用戶輸入的密碼進行hash處理,獲得密碼的hash值,而後將其存入數據庫中。

(2)密碼匹配(matches):用戶登陸時,密碼匹配階段並無進行密碼解密(由於密碼通過Hash處理,是不可逆的),而是使用相同的算法把用戶輸入的密碼進行hash處理,獲得密碼的hash值,而後將其與從數據庫中查詢到的密碼hash值進行比較。若是二者相同,說明用戶輸入的密碼正確。

這正是爲何處理密碼時要用hash算法,而不用加密算法。由於這樣處理即便數據庫泄漏,黑客也很難破解密碼(破解密碼只能用彩虹表)。

 

InMemoryUserDetailsManager 類是將用戶信息存放在內存中,上述代碼會在內存中建立兩個用戶,cralor用戶具備「USER」角色,admin用戶具備「ADMIN」角色。

到目前爲止,咱們的WebSecurityConfig僅包含有關如何驗證用戶身份的信息。Spring Security如何知道咱們要求全部用戶進行身份驗證?Spring Security如何知道咱們想要支持基於表單的身份驗證?緣由是WebSecurityConfigurerAdapterconfigure(HttpSecurity http)方法中提供了一個默認配置,咱們能夠自定義本身的配置。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
    }
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cralor").password(new BCryptPasswordEncoder().encode("123")).roles("USER").build());
        manager.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("123")).roles("ADMIN").build());
        return manager;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                        //以「/css/**」開頭的和「/index」資源不須要驗證,可直接訪問
                        .antMatchers("/css/**","/index").permitAll()
                        //任何以「/db/」開頭的URL都要求用戶擁有「ROLE_USER」角色
                        .antMatchers("/user/**").hasRole("USER")
                        //任何以「/db/」開頭的URL都要求用戶同時擁有「ROLE_ADMIN」和「ROLE_DBA」。因爲咱們使用的是hasRole表達式,所以咱們不須要指定「ROLE_」前綴。
                        .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
/*                        //確保對咱們的應用程序的任何請求都要求用戶進行身份驗證
                        .anyRequest().authenticated()*/
                        .and()
                //容許用戶使用基於表單的登陸進行身份驗證
                .formLogin()
                        //表單登錄地址「/login」,登陸失敗地址「/login-error」
                        .loginPage("/login").failureForwardUrl("/login-error")
                        .and()
                .logout()
                        //註銷地址
//                        .logoutUrl("/logout")
                        //註銷成功,重定向到首頁
                        .logoutSuccessUrl("/")
                        //指定一個自定義LogoutSuccessHandler。若是指定了,logoutSuccessUrl()則忽略。
                        //.logoutSuccessHandler(logoutHandler)
                        //指定HttpSession在註銷時是否使其無效。默認true
                        .invalidateHttpSession(true)
                        //容許指定在註銷成功時刪除的cookie的名稱。這是CookieClearingLogoutHandler顯式添加的快捷方式。
                        .deleteCookies("name","ss","aa")
                        .and()
                //異常處理會重定向到「/401」頁面
                .exceptionHandling().accessDeniedPage("/401")

        //      .httpBasic()//容許用戶使用HTTP基自己份驗證進行身份驗證
                ;
    }
}

在上述代碼中配置了相關的界面,如首頁、登錄頁,在Controller中作相關配置

@Controller
public class MainController {

    @RequestMapping("/")
    public String root(){
        return "redirect:/index";
    }

    @RequestMapping("/index")
    public String index(){
        return "index";
    }

    @RequestMapping("user/index")
    public String userIndex(){
        return "user/index";
    }

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

    @RequestMapping("login-error")
    public String loginError(Model model){
        model.addAttribute("loginError",true);
        return "login";
    }

    /**
     * 退出登錄兩種方式,一種在配置類設置,一種在這裏寫就不須要配置了
     *
     * 這裏 首先咱們在使用SecurityContextHolder.getContext().getAuthentication() 以前校驗該用戶是否已經被驗證過。
     * 而後調用SecurityContextLogoutHandler().logout(request, response, auth)  來退出
     *
     * logout 調用流程:
     *
     *  1 將 HTTP Session 做廢,解綁其綁定的全部對象。
     *
     *  2 從SecurityContext移除Authentication 防止併發請求的問題。
     *
     *  3 顯式地清楚當前線程的上下文裏的值。
     *
     * 在應用的其餘地方再也不須要處理 退出。
     *
     * 注意:你甚至都不須要在你的spring多添加任何配置(不論是基於註解仍是基於xml)。
     *
     * @param request
     * @param response
     * @return
     */
/*  @RequestMapping("logout")
    public String logoutPage (HttpServletRequest request, HttpServletResponse response) {

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null) {
            new SecurityContextLogoutHandler().logout(request, response, auth);
        }
        return "redirect:/login";
    }*/

        @GetMapping("/401")
    public String accessDenied(){
        return "401";
    }
}

編寫相關界面

在application.yml中配置thymeleaf引擎

spring:
  thymeleaf:
    mode: HTML5
    encoding: UTF-8
    cache: false

 登錄界面login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login page</title>
    <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}"/>
</head>
<body>
    <h1>Login page</h1>
    <p>User角色用戶: cralor / 123</p>
    <p>Admin角色用戶: admin / 123</p>
    <p th:if="${loginError}" class="error">用戶名或密碼錯誤</p>
    <form th:action="@{login}" method="post">
        <label for="username">用戶名:</label>
        <input type="text" id="username" name="username" autofocus="autofocus">
        <label for="password">密碼:</label>
        <input type="password" id="password" name="password">
        <input type="submit" value="登錄">
    </form>
    <a href="/index" th:href="@{/index}">返回首頁</a>
</body>
</html>

首頁index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <title>Hello Spring Security</title>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
</head>
<body>

<h1>Hello Spring Security</h1>
<p>這個界面沒有受保護,你能夠進已被保護的界面.</p>

<div th:fragment="logout"  sec:authorize="isAuthenticated()">
    登陸用戶: <span sec:authentication="name"></span> |
    用戶角色: <span sec:authentication="principal.authorities"></span>
    <div>
        <form action="#" th:action="@{/logout}" method="post">
            <input type="submit" value="登出" />
        </form>
    </div>
</div>

<ul>
    <li>點擊<a href="/user/index" th:href="@{/user/index}">去/user/index保護的界面</a></li>
</ul>
</body>
</html>

權限不夠顯示的頁面401.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

<body>
    <div >
          <div >
            <h2> 權限不夠</h2>
        </div>
        <div sec:authorize="isAuthenticated()">
            <p>已有用戶登陸</p>
            <p>用戶: <span sec:authentication="name"></span></p>
             <p>角色: <span sec:authentication="principal.authorities"></span></p>
        </div>
         <div sec:authorize="isAnonymous()">
            <p>未有用戶登陸</p>
        </div>
        <p>
            拒絕訪問!
        </p>
    </div>

</body>
</html>

用戶首頁/user/index.html,被Spring Security保護,只有擁有「USER」角色的用戶才能訪問。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Hello Spring Security</title>
        <meta charset="utf-8" />
        <link rel="stylesheet" href="/css/main.css" th:href="@{/css/main.css}" />
    </head>
    <body>
        <div th:substituteby="index::logout"></div>
        <h1>這個界面是被保護的界面</h1>
        <p><a href="/index" th:href="@{/index}">返回首頁</a></p>

        <p><a  href="/blogs" th:href="@{/blogs}">管理博客</a></p>
    
    </body>
</html>

 啓動工程,在瀏覽器訪問localhost:8080,會被重定向到localhost:8080/index頁面。

 

 點擊 「去/user/index保護的界面」 ,因爲「/user/index」界面須要「USER」權限,但尚未登錄,會被重定向到登錄界面「/login.html」。

  

使用具備「USER」權限的cralor用戶登錄,登錄成功後會被重定向到「localhost:8080/user/index」界面。

退出登錄,用admin用戶登錄,該用戶沒有「USER」權限,此時會返回權限不足界面。 

 

修改WebSecurityConfig,給admin用戶加上「USER」角色。

@Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cralor").password(new BCryptPasswordEncoder().encode("123")).roles("USER").build());
        manager.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("123")).roles("ADMIN","USER").build());
        return manager;
    }

 並再次訪問「localhost:8080/user/index」,可正常顯示。

Spring Security方法級保護

 WebSecurityConfig類加上@EnableGlobalMethodSecurity註解,能夠開啓方法級保護。括號後面的參數可選,可選參數以下:

  • prePostEnabled:Spring Security的Pre和Post註解是否可用,即@PreAuthorize@PostAuthorize是否可用;

  • secureEnabled:Spring Security的@Secured註解是否可用;

  • jsr250Enabled:Spring Security對JSP-250的註解是否可用。

通常只用到prePostEnabled。由於@PreAuthorize註解和@PostAuthorize註解更適合方法級安全控制,而且支持Spring EL表達式,適合Spring開發者。其中,@PreAuthorize註解會在進入方法前進行權限驗證。@PostAuthorize註解在方法執行後再進行權限驗證,此註解應用場景不多。如何使用方法級保護註解呢?例如:

  • 有權限字符串ROLE_ADMIN,在方法上能夠寫@PreAuthorize("harRole('ADMIN')"),此處爲權限名,也能夠寫爲@PreAuthorize("harAuthority('ROLE_ADMIN')"),驗證權限名和權限字符串兩者等價;

  • 加多個權限,能夠寫爲@PreAuthorize("harRole('ADMIN','USER')"),也可寫爲@PreAuthorize("harAuthority('ROLE_ADMIN','ROLE_USER')")

 

新添加一個API接口,在該接口上添加權限註解。寫一個Blog文章列表的API接口,只有管理員權限的用戶才能夠刪除Blog。

新建實體類Blog

public class Blog {

    private Long id;
    private String name;
    private String content;

    public Blog(){}

    public Blog(Long id, String name, String content) {
        this.id = id;
        this.name = name;
        this.content = content;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

新建IBlogService接口類(沒有DAO層操做數據庫,只是在內存中)。

public interface IBlogService {
    /**
     * 獲取全部Blog
     * @return
     */
    List<Blog> getBlogs();
    /**
     * 根據ID獲取Blog
     * @param id
     */
    void deleteBlog(long id);
}

實現類BlogServiceImpl,在構造方法添加兩個Blog對象。

@Service
public class BlogServiceImpl implements IBlogService {

    private List<Blog> list=new ArrayList<>();

    public BlogServiceImpl(){
        list.add(new Blog(1L, " spring in action", "good!"));
        list.add(new Blog(2L,"spring boot in action", "nice!"));
    }

    @Override
    public List<Blog> getBlogs() {
        return list;
    }

    @Override
    public void deleteBlog(long id) {
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            Blog blog= (Blog) iter.next();
            if (blog.getId()==id){
                iter.remove();
            }
        }
    }
}

BlogController寫兩個API接口,一個獲取全部Blog列表,第二個根據ID刪除Blog,須要有ADMIN權限。

@RestController
@RequestMapping("/blogs")
public class BlogController {

    @Autowired
    IBlogService blogService;

    @GetMapping
    public ModelAndView list(Model model) {

        List<Blog> list =blogService.getBlogs();
        model.addAttribute("blogsList", list);
        return new ModelAndView("blogs/list", "blogModel", model);
    }

    /**
     * 須要擁有「ADMIN」角色權限才能夠訪問
     * @param id 要刪除的Blog的id
     * @param model 返回的視圖模型
     * @return 返回頁面
     */
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    @GetMapping(value = "/{id}/deletion")
    public ModelAndView delete(@PathVariable("id") Long id, Model model) {
        blogService.deleteBlog(id);
        model.addAttribute("blogsList", blogService.getBlogs());
        return new ModelAndView("blogs/list", "blogModel", model);
    }
}

博客列表界面

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

<body>

    登陸用戶: <span sec:authentication="name"></span> |
    用戶角色: <span sec:authentication="principal.authorities"></span>
<br>
<br>

<div>
    <table >
        <thead>
        <tr>
            <td>博客編號</td>
            <td>博客名稱</td>
            <td>博客描述</td>
        </tr>
        </thead>
        <tbody>
        <tr th:each="blog: ${blogModel.blogsList}">
            <td th:text="${blog.id}"></td>
            <td th:text="${blog.name}"></td>
            <td th:text="${blog.content}"></td>
            <td>
                <div >
                    <a th:href="@{'/blogs/' + ${blog.id}+'/deletion'}">
                        刪除
                    </a>
                </div>
            </td>
        </tr>
        </tbody>
    </table>
</div>

</body>
</html>

啓動程序,使用admin用戶登錄,進入管理博客界面。

點擊刪除,刪除編號爲2的博客,刪除成功後如圖:

從新用cralor用戶登陸,點擊刪除時,會顯示權限不足。

可見,在方法級別上的安全驗證時經過相關的註解和配置來實現的。註解寫在Controller層仍是Service層都生效。

從數據庫讀取用戶的認證信息

數據庫MySql,ORM框架JPA。

添加MySql和JPA的依賴

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

配置數據庫的相關配置

spring:
  thymeleaf:
    mode: HTML5
    encoding: UTF-8
    cache: false
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/spring-security?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: root
    password: ok
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

建立User實體類,使用JPA的@Entity註解,代表該Java對象會被映射到數據庫。id採用的生成策爲自增長,包含username和password兩個字段,其中authorities爲權限點的集合。

@Entity
public class User implements UserDetails, Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false,  unique = true)
    private String username;

    @Column
    private String password;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
            inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
    private List<Role> authorities;

    public User() {
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(List<Role> authorities) {
        this.authorities = authorities;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

User類實現了UserDetails接口,該接口是實現Spring Security認證信息的核心接口。其中,getUsername()方法爲UserDetails接口的方法,這個方法不必定返回username,也能夠返回其餘信息,如手機號碼,郵箱地址等。getAuthorities()方法返回的是該用戶設置的權限信息,在本例中返回的是從數據庫讀取的該用戶的全部角色信息,權限信息也能夠是用戶的其餘信息。另外須要讀取密碼。最後幾個方法通常都返回true,可根據本身的需求進行業務判斷。

UserDetails接口源碼以下

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

新建Role類,實現GrantedAuthority接口,重寫了getAuthority()方法。權限能夠爲任何字符串,不必定是角色名的字符串,關鍵是getAuthority()方法如何實現。本例中權限是從數據庫中讀取Role表的name字段。

@Entity
public class Role implements GrantedAuthority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getAuthority() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

編寫DAO層,UserDao繼承了JpaRepository默認實現了大多數單表查詢的操做。UserDao中自定義一個根據username獲取user的方法,因爲JPA已經自動實現了根據username字段查找用戶的方法,所以不須要額外的工做。

public interface UserDao extends JpaRepository<User, Long> {

    User findByUsername(String username);
}

編寫Service層,須要實現UserDetailsService接口,該接口根據用戶名獲取用戶的全部信息,包括用戶信息和權限。

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private UserDao userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByUsername(username);
    }
}

最後修改Spring Security配置類WebSecurityConfig ,讓Spring Security從數據庫中獲取用戶的認證信息。

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

/*    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
        auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
    }
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("cralor").password(new BCryptPasswordEncoder().encode("123")).roles("USER").build());
        manager.createUser(User.withUsername("admin").password(new BCryptPasswordEncoder().encode("123")).roles("ADMIN","USER").build());
        return manager;
    }*/

    //一、@Autowired和@Qualifier結合使用
//    @Qualifier("userServiceImpl")
//    @Autowired

    //二、使用@Resource
    @Resource
    UserDetailsService userDetailsService;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{

        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());

    }

 

 

由於Spring Boot會自動配置一些Bean,上邊用到的UserDetailsService的bean已經在UserDetailsServiceAutoConfiguration類的inMemoryUserDetailsManager()方法自動裝配好了。此時單獨使用@Autowired註解自動注入UserDetailsService會報錯:

Could not autowire. There is more than one bean of 'UserDetailsService' type.
Beans:
inMemoryUserDetailsManager   (UserDetailsServiceAutoConfiguration.class) userServiceImpl   (UserServiceImpl.java) 

能夠結合使用 @Qualifier("userServiceImpl")和 @Autowired(須要知道已經自動裝配好的Bean的名字),或者使用@Resource,建議使用@Resource。

 

在啓動程序前須要在數據庫中建庫、建表、初始化數據。建庫腳本以下:

CREATE DATABASE `spring-security` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

建表腳本以下:

/*
 Navicat Premium Data Transfer

 Source Server         : root
 Source Server Type    : MySQL
 Source Server Version : 50721
 Source Host           : localhost:3306
 Source Schema         : spring-security

 Target Server Type    : MySQL
 Target Server Version : 50721
 File Encoding         : 65001

 Date: 31/07/2018 17:36:26
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_USER');
INSERT INTO `role` VALUES (2, 'ROLE_ADMIN');

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'cralor', '$2a$10$E9Ex7Yp4JZlaslgXCkkl/ugt7hwfoT/tPRHjlYyC8JnsPeLE1Yry2');
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$kcyDOQPHnpFEYYKuRTayYupejdw5HFUF6/zm0yT5jog/waRwc8THi');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  INDEX `user_id`(`user_id`) USING BTREE,
  INDEX `role_id`(`role_id`) USING BTREE,
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1);
INSERT INTO `user_role` VALUES (2, 1);
INSERT INTO `user_role` VALUES (2, 2);

SET FOREIGN_KEY_CHECKS = 1;

由於Spring Security要求密碼必需加密,因此咱們直接存密碼」123「使用 BCryptPasswordEncoder 進行hash運算後的hash值。

新建一個測試類,對」123「進行hash運算。

@RunWith(SpringRunner.class)
@SpringBootTest
public class Springboot3ApplicationTests {

    @Test
    public void contextLoads() {
        
        BCryptPasswordEncoder util = new BCryptPasswordEncoder();
        for(int i = 0 ; i < 10; i ++){
            System. out.println("經hash後的密碼爲:"+util.encode("123" ));
        }
    }

}

輸出結果以下,隨便複製兩個存到數據庫便可(具體原理請自行百度🙃)。

 

啓動程序,瀏覽器訪問http://localhost:8080,會發現跟以前存放在內存中的用戶信息的認證效果是同樣的。

 

  本案例代碼地址:https://github.com/cralor7/springcloud/tree/master/chap13-security

 

 

相關文章
相關標籤/搜索