Spring Security登錄賬戶自定義與數據持久化(5)

来源:https://www.cnblogs.com/liwenruo/archive/2022/08/03/16547404.html
-Advertisement-
Play Games

1.用戶定義 在前面的案例中,我們的登錄用戶是基於配置文件來配置的(本質是基於記憶體),但是在實際開發中,這種方式肯定是不可取的,在實際項目中,用戶信息肯定要存入資料庫之中。 Spring Security支持多種用戶定義方式,接下來我們就逐個來看一下這些定義方式。通過前面的介紹(參見3小節),大家對 ...


1.用戶定義

在前面的案例中,我們的登錄用戶是基於配置文件來配置的(本質是基於記憶體),但是在實際開發中,這種方式肯定是不可取的,在實際項目中,用戶信息肯定要存入資料庫之中。

  Spring Security支持多種用戶定義方式,接下來我們就逐個來看一下這些定義方式。通過前面的介紹(參見3小節),大家對於UserDetailsService以及它的子類都有了一定的瞭解, 自定義用戶其實就是使用UserDetailsService的不同實現類來提供用戶數據,同時將配置好的 UserDetailsService 配置給 AuthenticationManagerBuilder,系統再將 UserDetailsSeivice 提供給 AuthenticationProvider 使用,

  1.1 基於記憶體

  前面案例中用戶的定義本質上還是基於記憶體,只是我們沒有將InMemoryUserDetailsManager類明確抽出來自定義,現在我們通過自定義InMemoryUserDetailsManager來看一下基於記憶體的用戶是如何自定義的。

    重寫 WebSecurityConfigurerAdapter 類的 configure(AuthenticationManagerBuilder)方法,內容如下:

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("buretuzi").password("{noop}123456").roles("admin").build());
    manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
    auth.userDetailsService(manager);
}

  首先構造了一個InMemoryUserDetailsManager實例,調用該實例的createUser方法來創建用戶對象,我們在這裡分別設置了用戶名、密碼以及用戶角色。需要註意的是,用戶密碼加了 一個{noop}首碼,表示密碼不加密,明文存儲(關於密碼加密問題,會在後面的章節中專門介紹)。

  配置完成後,啟動項目,此時就可以使用這裡配置的兩個用戶登錄了。

  InMemoryUserDetailsManager 的實現原理很簡單,它間接實現了 UserDetailsService 介面並重寫了它裡邊的 loadUserByUsername方法,同時它裡邊維護了 一個HashMap變數,Map的 key 就是用戶名,value則是用戶對象,createUser就是往這個Map中存儲數據,loadUserByUsername方法則是從該Map中讀取數據,這裡的源碼比較簡單,就不貼出來了,讀者可以自行查看。

  1.2  基於JdbcUserDetailsManager

  JdbcUserDetailsManager支持將用戶數據持久化到資料庫,同時它封裝了一系列操作用戶的方法,例如用戶的添加、更新、查找等。

   Spring Security 中為 JdbcUserDetailsManager 提供了資料庫腳本,位置在 org/springframework/security/core/userdetails/jdbc/users.ddl 內容如下:(註意將varchar_ignorecase改為varchar

create table users(username varchar_ignorecase(50) not null primary key,
                   password varchar_ignorecase(500) not null,
                   enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,
                          authority varchar_ignorecase(50) not null,
                          constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

  可以看到這裡一共創建了兩張表,users表就是存放用戶信息的表,authorities則是存放用戶角色的表。但是大家註意SQL的數據類型中有一個varchar_ignorecase,這個其實是針對 HSQLDB 的數據類型,我們這裡使用的是MySQL資料庫,所以這裡手動將varchar_ignorecase 類型修改為varchar類型,然後去資料庫中執行修改後的腳本。

  另一方面,由於要將數據存入資料庫中,所以我們的項目也要提供資料庫支持, JdbcUserDetailsManager底層實際上是使用JdbcTemplate來完成的,所以這裡主要添加兩個依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

  然後在resources/application.yml中配置資料庫連接信息:

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      password: 123456

  配置完成後,我們重寫WebSecurityConfigurerAdapter類的 configure(AuthenticationManagerBuilder auth)方法,內容如下(註意版本,不得低於以下版本):

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.3.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.3.6.RELEASE</version>
</dependency>
package com.intehel.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import javax.sql.DataSource;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    DataSource dataSource;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
        if (!manager.userExists("buretuzi")){
            manager.createUser(User.withUsername("buretuzi").password("{noop}123456").roles("admin").build());
        }
        if (!manager.userExists("song")){
            manager.createUser(User.withUsername("sang").password("{noop}123").roles("user").build());
        }
        auth.userDetailsService(manager);
    }
}
  • 當引入spring-boot-starter-jdbc並配置了資料庫連接信息後,一個DataSource實例就有了,這裡首先引入DataSource實例。
  • 在 configure 方法中,創建一個 JdbcUserDetailsManager 實例,在創建時傳入 DataSource 實例。通過userExists方法可以判斷一個用戶是否存在,該方法本質上就是去資料庫中査詢對應的用戶;如果用戶不存在,則通過createUser方法可以創建一個用戶,該方法本質上就是向資料庫中添加一個用戶。
  • 最後將manager實例設置到auth對象中。

  配置完成後,重啟項目,如果項目啟動成功,資料庫中就會自動添加進來兩條數據,如圖2-22、圖2-23所示。

  

圖 2-22

圖 2-23

  此時,我們就可以使用buretuzi/123456,sang/123進行登錄測試了。

  在 JdbcUserDetailsManager 的繼承體系中,首先是 JdbcDaoImpl 實現了 UserDetailsService 介面,並實現了基本的loadUserByUsername方法,JdbcUserDetailsManager則繼承自 JdbcDaoImpl,同時完善了資料庫操作,又封裝了用戶的增刪改査方法,這裡,我們以 loadUserByUsername為例,看一下源碼,其餘的增刪改操作相對來說都比較容易,這裡就不再贅述了。

  JdbcDaoImpl#loadUserByUsername:

public class JdbcDaoImpl extends JdbcDaoSupport implements UserDetailsService, MessageSourceAware {
    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";
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private String authoritiesByUsernameQuery = "select username,authority from authorities where username = ?";
    private String groupAuthoritiesByUsernameQuery = "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";
    private String usersByUsernameQuery = "select username,password,enabled from users where username = ?";
    private String rolePrefix = "";
    private boolean usernameBasedPrimaryKey = true;
    private boolean enableAuthorities = true;
    private boolean enableGroups;
    
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        List<UserDetails> users = this.loadUsersByUsername(username);
        if (users.size() == 0) {
            this.logger.debug("Query returned no results for user '" + username + "'");
            throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.notFound", new Object[]{username}, "Username {0} not found"));
        } else {
            UserDetails user = (UserDetails)users.get(0);
            Set<GrantedAuthority> dbAuthsSet = new HashSet();
            if (this.enableAuthorities) {
                dbAuthsSet.addAll(this.loadUserAuthorities(user.getUsername()));
            }

            if (this.enableGroups) {
                dbAuthsSet.addAll(this.loadGroupAuthorities(user.getUsername()));
            }

            List<GrantedAuthority> dbAuths = new ArrayList(dbAuthsSet);
            this.addCustomAuthorities(user.getUsername(), dbAuths);
            if (dbAuths.size() == 0) {
                this.logger.debug("User '" + username + "' has no authorities and will be treated as 'not found'");
                throw new UsernameNotFoundException(this.messages.getMessage("JdbcDaoImpl.noAuthority", new Object[]{username}, "User {0} has no GrantedAuthority"));
            } else {
                return this.createUserDetails(username, user, dbAuths);
            }
        }
    }

    protected List<UserDetails> loadUsersByUsername(String username) {
        return this.getJdbcTemplate().query(this.usersByUsernameQuery, new String[]{username}, (rs, rowNum) -> {
            String username1 = rs.getString(1);
            String password = rs.getString(2);
            boolean enabled = rs.getBoolean(3);
            return new User(username1, password, enabled, true, true, true, AuthorityUtils.NO_AUTHORITIES);
        });
    }
}
  • 首先根據用戶名,調用loadUserByUsername方法去資料庫中查詢用戶,查詢出來的是一個List集合,集合中如果沒有數據,說明用戶不存在,則直接拋出異常,
  • 如果集合中存在數據,則將集合中的第一條數據拿出來,然後再去查詢用戶角色, 最後根據這些信息創建一個新的UserDetails出來。
  • 需要註意的是,這裡還引入了分組的概念,不過考慮到JdbcUserDetailsManager並非我們實際項目中的主流方案,因此這裡不做過多介紹。

  這就是使用JdbcUserDetailsManager做數據持久化。這種方式看起來簡單,都不用開發者自己寫SQL,但是局限性比較大,無法靈活地定義用戶表、角色表等,而在實際開發中,我們還是希望能夠靈活地掌控數據表結構,因此JdbcUserDetailsManager使用場景非常有限。

  1.3 基於 MyBatis

  使用MyBatis做數據持久化是目前大多數企業應用釆取的方案,Spring Security中結合 MyBatis可以靈活地定製用戶表以及角色表,我們對此進行詳細介紹。

  首先需要設計三張表,分別是用戶表、角色表以及用戶角色關聯表,三張表的關係如圖 2-24所示。

  

圖 2-24

  用戶和角色是多對多的關係,我們使用user_role來將兩者關聯起來。 資料庫腳本如下:

CREATE TABLE `role`(
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(32) DEFAULT NULL,
	`nameZh` VARCHAR(32) DEFAULT NULL,
	PRIMARY KEY(`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8

CREATE TABLE `user` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`username` VARCHAR(32) DEFAULT NULL,
	`password` VARCHAR(255) DEFAULT NULL,
	`enabled` TINYINT(1) DEFAULT NULL,
	`accountNonExpired` TINYINT(1) DEFAULT NULL,
	`accountNonLocked` TINYINT(1) DEFAULT NULL,
	`credentialsNonExpired` TINYINT(1) DEFAULT NULL,
	PRIMARY KEY(`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8

CREATE TABLE `user_role`(
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`uid` INT(11) DEFAULT NULL,
	`rid` INT(11) DEFAULT NULL,
	PRIMARY KEY(`id`),
	KEY `uid` (`uid`),
	KEY `rid` (`rid`)
)ENGINE=INNODB DEFAULT CHARSET=utf8

  對於角色表,三個欄位從上往下含義分別為角色id、角色英文名稱以及角色中文名稱, 對於用戶表,七個欄位從上往下含義依次為:用戶id、用戶名、用戶密碼、賬戶是否可用、賬戶是否沒有過期、賬戶是否沒有鎖定以及憑證(密碼)是否沒有過期。

  資料庫創建完成後,可以向資料庫中添加幾條模擬數據,代碼如下:

INSERT INTO `role` (`id`,`name`,`nameZh`)
VALUES
	(1,'ROLE_dba','資料庫管理員'),
	(2,'ROLE_admin','系統管理員'),
	(3,'ROLE_user','用戶');

INSERT INTO `user` (`id`,`username`,`password`,`enabled`,`accountNonExpired`,`accountNonLocked`,`credentialsNonExpired`)
VALUES
	(1,'root','{noop}123',1,1,1,1),
	(2,'admin','{noop}123',1,1,1,1),
	(3,'sang','{noop}123',1,1,1,1);

INSERT INTO `user_role` (`id`,`uid`,`rid`)
VALUES
	(1,1,1),
	(2,1,2),
	(3,2,2),
	(4,3,3);

  這樣,資料庫的準備工作就算完成了。

  在Spring Security項目中,我們需要引入MyBatis和MySQL依賴,代碼如下:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.2.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

  同時在resources/application.yml中配置資料庫基本連接信息:

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      password: 123456

  接下來創建用戶類和角色類:

package com.intehel.demo.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import lombok.Data;

@Data
public class User implements UserDetails {
    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    private List<Role> roles = new ArrayList<Role>();
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

 

package com.intehel.demo.domain;
import lombok.Data;
@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

  自定義用戶類需要實現UserDetails介面,並實現介面中的方法,這些方法的含義我們在 3小節中已經介紹過了,這裡不再贅述。其中roles屬性用來保存用戶所具備的角色信息, 由於系統獲取用戶角色調用的方法是getAuthorities,所以我們在getAuthorities方法中,將roles 中的角色轉為系統可識別的對象並返回。

  接下來我們自定義UserDetailsService以及對應的資料庫查詢方法:

package com.intehel.demo.mapper;

import com.intehel.demo.domain.Role;
import com.intehel.demo.domain.User;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface UserMapper{
    List<Role> getRolesByUid(Integer id);
    User loadUserByUsername(String username);
}
package com.intehel.demo.service;

import com.intehel.demo.domain.User;
import com.intehel.demo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        user.setRoles(userMapper.getRolesByUid(user.getId()));
        return user;
    }
}

  自定義 MyUserDetailsService實現UserDetailsSeivice介面,並實現該介面中的方法。 loadUserByUsername方法經過前面章節的講解,相信大家已經很熟悉了,該方法就是根據用戶名去資料庫中載入用戶,如果從資料庫中沒有査到用戶,則拋出UsemameNotFoundException 異常;如果査詢到用戶了,則給用戶設置roles屬性。

  UserMapper中定義兩個方法用於支持MyUserDetailsService中的査詢操作。

  最後,在UserMapper.xml中定義查詢SQL,代碼如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.intehel.demo.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="com.intehel.demo.domain.User">
        select * from `user` where username = #{username}
    </select>

    <select id="loadUserByUsername" resultType="com.intehel.demo.domain.Role">
        select r.* from role r,user_role ur where r.`id`=ur.`rid`
    </select>
</mapper>

  將mylogin.html放在 resources/templates/ 下,mylogin.html如下

查看代碼
 <!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登錄</title>
    <link href="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
    <script src="//maxcdn.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
</head>
<style>
    #login .container #login-row #login-column #login-box {
        border: 1px solid #9c9c9c;
        background-color: #EAEAEA;
    }
</style>
<body>
<div id="login">
    <div class="container">
        <div id="login-row" class="row justify-content-center align-items-center">
            <div id="login-column" class="col-md-6">
                <div id="login-box" class="col-md-12">
                    <form id="login-form" class="form" action="/doLogin" method="post">
                        <h3 class="text-center text-info">登錄</h3>
                        <!--/*@thymesVar id="SPRING_SECURITY_LAST_EXCEPTION" type="com"*/-->
                        <div th:text="${SPRING_SECURITY_LAST_EXCEPTION}"></div>
                        <div class="form-group">
                            <label for="username" class="text-info">用戶名:</label><br>
                            <input type="text" name="uname" id="username" class="form-control">
                        </div>
                        <div class="form-group">
                            <label for="password" class="text-info">密碼:</label><br>
                            <input type="text" name="passwd" id="password" class="form-control">
                        </div>
                        <div class="form-group">
                            <input type="submit" name="submit" class="btn btn-info btn-md" value="登錄">
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

  為了方便,我們將UserMapper.xml文件放在resources/mapper下,UsetMapper介面放在mapper包下。為了防止 Maven打包時自動忽略了 XML文件,還需要在application.yml中添加mapper-locations配置:

查看代碼
# 應用名稱
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      password: 123456
  security:
    user:
      name: buretuzi
      password: 123456
  application:
    name: demo
  thymeleaf:
    mode: HTML
    encoding: UTF-8
    servlet:
      content-type: text/html
    cache: false
    prefix: classpath:/templates/
        
# 應用服務 WEB 訪問埠
server:
  port: 8080
mybatis:
  # spring boot集成mybatis的方式列印sql
  mapper-locations: classpath:/mapper/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

  最後一步,就是在 SecurityConfig 中註入 UserDetailsService:

查看代碼
 package com.intehel.demo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.intehel.demo.handler.MyAuthenticationFailureHandler;
import com.intehel.demo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.OrRequestMatcher;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    MyUserDetailsService myUserDetailsService;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/mylogin.html")
                .loginProcessingUrl("/doLogin")
                .defaultSuccessUrl("/index.html")
                .failureHandler(new MyAuthenticationFailureHandler())
                .usernameParameter("uname")
                .passwordParameter("passwd")
                .permitAll()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(new AntPathRequestMatcher("/logout1","GET"),
                        new AntPathRequestMatcher("/logout2","POST")))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout1註銷成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .defaultLogoutSuccessHandlerFor((req,resp,auth)->{
                    resp.setContentType("application/json;charset=UTF-8");
                    Map<String,Object> result = new HashMap<String,Object>();
                    result.put("status",200);
                    result.put("msg","使用logout2註銷成功!");
                    ObjectMapper om = new ObjectMapper();
                    String s = om.writeValueAsString(result);
                    resp.getWriter().write(s);
                },new AntPathRequestMatcher("/logout1","GET"))
                .and()
                .csrf().disable();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(myUserDetailsService);
    }
}

   配置UserDetailsService的方式和前面配置JdbcUserDetailsManager的方式基本一致,只不過配置對象變成了 myUserDetailsService而己。至此,整個配置工作就完成了。

  接下來啟動項目,利用資料庫中添加的模擬用戶進行登錄測試,就可以成功登錄了,測試方式和前面章節一致,這裡不再贅述。

  1.4  基於 Spring Data JPA

    考慮到在Spring Boot技術棧中也有不少人使用Spring Data JPA,因此這裡針對Spring Security+Spring Data JPA也做一個簡單介紹,具體思路和基於MyBatis的整合類似。

   首先引入Spring Data JPA的依賴和MySQL依賴:

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

  然後在resources/application.yml中配置資料庫和JPA,代碼如下:

spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
      username: root
      password: 123456
  jpa:
    database: mysql
    database-platform: mysql
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.Mysql8Dialect

  據庫的配置還是和以前一樣,JPA的配置則主要配置了資料庫平臺,數據表更新方式、 是否列印SQL以及對應的資料庫方言。

  使用Spring Data JPA的好處是我們不用提前準備SQL腳本,所以接下來配置兩個資料庫實體類即可:

package com.intehel.demo.domain;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity
@Data
public class User implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean accountNonExpired;
    private Boolean accountNonLocked;
    private Boolean credentialsNonExpired;
    @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.PERSIST)
    private List<Role> roles;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return accountNonExpired;
    }
    @Override
    public boolean isAccountNonLocked() {
        return accountNonLocked;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return credentialsNonExpired;
    }
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}
package com.intehel.demo.domain;

import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Data
@Entity(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String name;
    private String nameZh;
}

  這兩個實體類和前面MyBatis中實體類的配置類似,需要註意的是roles屬性上多了一個 多對多配置。

  接下來配置UserDetailsService,並提供數據查詢方法:

package com.intehel.demo.dao;

import com.intehel.demo.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserDao extends JpaRepository<User,Integer> {
    User findUserByUsername(String username);
}
package com.intehel.demo.Service;

import com.intehel.demo.dao.UserDao;
import com.intehel.demo.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    UserDao userDao;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userDao.findUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用戶不存在");
        }
        return user;
    }
}

  MyUserDetailsService的定義也和前面的類似,不同之處在於數據查詢方法的變化。定義 UserDao 繼承自 JpaRepository,並定義一個 findUserByUsername 方法,剩下的事情 Spring Data JPA框架會幫我們完成。

  最後,再在 SecurityConfig 中配置 MyUserDetailsService配置方式和 MyBatis 一模一樣, 這裡就不再把代碼貼岀來了。使用了 Spring Data JPA之後,當項目啟動時,會自動在資料庫中創建相關的表,而不用我們自己去寫腳本,這也是使用Spring Data JPA的方便之處。

  為了測試方便,我們可以在單元測試中執行如下代碼,向資料庫中添加測試數據:

package com.intehel.demo;

import com.intehel.demo.dao.UserDao;
import com.intehel.demo.domain.Role;
import com.intehel.demo.domain.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
class DemoApplicationTests {
    @Autowired
    UserDao userDao;
    @Test
    void contextLoads() {
        User user1 = new User();
        user1.setUsername("buretuzi");
        user1.setPassword("{noop}123");
        user1.setAccountNonExpired(true);
        user1.setAccountNonLocked(true);
        user1.setCredentialsNonExpired(true);
        user1.setEnabled(true);
        List<Role> roles = new ArrayList<>();
        Role r1 = new Role();
        r1.setName("ROLE_admin");
        r1.setNameZh("管理員");
        roles.add(r1);
        user1.setRoles(roles);
        userDao.save(user1);
    }

}

  測試數據添加成功之後,接下來啟動項目,使用測試數據進行登錄測試,具體測試過程就不再贅述了。

  至此,四種不同的用戶定義方式就介紹完了。這四種方式,異曲同工,只是數據存儲的方式不一樣而已,其他的執行流程都是一樣的


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Ajax&Vue 1、Ajax 1.1、概述 AJAX(Asynchronous JavaScript And XML):非同步的JavaScript和XML 作用 1.與伺服器進行數據交換:通過ajax可以給伺服器發送請求,伺服器將數據直接響應回給瀏覽器。 如圖是沒使用Ajax的時候,各個請求之間的 ...
  • svg中有一個標簽path,主要表示路徑的意思,當我們畫一個半圓的時候,如圖: <svg> <!-- 背景布 --> <path d="M 150 0 L 150 200 M 0 100 L 300 100" stroke="black" stroke-width="1" /> <!-- 半圓對象 ...
  • vuex狀態管理 概述 vuex主要由state,mutations,actions,getters四部分構成(modules本文暫不闡述) state 全局狀態,只讀 組件讀取state方式 根組件上引入並掛載store對象,其他組件在計算屬性通過$store獲取狀態 可以在計算屬性上使用mapS ...
  • 封裝了一個基於vue的公用SvgIcon組件庫,只需要降svg文件放入指定文件夾,就可以通過文件名綁定到SvgIcon組件上使用該文件。 ...
  • 1. 表格標簽 1.1主要作用 顯示和展示數據美觀、良好。 1.2基本語法 <table> <tr> <td>單元格內的文字</td> ... </tr> ... </table> <table></table>用來定義表格的標簽 <tr></tr>標簽用於定義表格中的行,必須鑲嵌在<table>< ...
  • 在設計網頁的時候常常遇到這種情況:一個元素使用的樣式與另一個元素完全相同,但又添加了額外的樣式。 通常會在 HTML 中給元素定義兩個 class,一個通用樣式,一個特殊樣式。 普通CSS的實現 接下來以警告框為例進行講,解4種類型 | 類型 | 說明 | | | | | info | 信息!請註意 ...
  • 如果你有玩過 🎮 《王者榮耀》、《陰陽師》 等手游,一定註意到過它的啟動動畫、皮膚立繪卡片等場景,經常採用靜態底圖加局部液態流動效果的簡單動畫,本文使用前端開發技術,結合 SVG 和 CSS 來實現類似的液化流動效果。本文包含的知識點主要包括:mask-image 遮罩、feTurbulence ... ...
  • 一個工作薄中快速新建多個數據表 一、建立數據源數表 將所有數據統一錄入到本數據源表中,併進行統一排列數據。 註意:這裡的序號很重要,是對應後面工作表格獲取數據的來源,具體見公式設置。 二、建立一個表格模板 ① 將自己需要的表格進行製作及調整格式。 註意:做好表格列印格式調整。 ② 表格做好讀取表格名 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...