springboot+security自定義登錄-1-基礎-自定義用戶和登錄界面


為何學習spring security? 理由如下:

1)雖然可以不用,但難免部分客戶又要求

2)某種程度上,security還是不錯的,譬如csrf,oauth等等,省了一些功夫。

3)雖然spring security 比較龐雜,甚至有些臃腫,但權衡之下,還是可以一學!。

 

根據很多網絡例子和書籍來試驗,都沒有成功,原因可能是:

1)某些地方配置錯了

2)使用的版本和他人不同

費了不少功夫。

 

一氣之下,直接使用2.3.4的版本,成功了!

毫無疑問,2.3.4比以往的更加人性化。

以下內容比較長,可能需要耗費10分鍾以上時間閱讀。

 

自定義的關鍵在於幾點。

 

一、pom配置+spring配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>learning</groupId>
    <artifactId>secutiry</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>secutiry</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</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.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--  支持jsp -->
        <!-- 添加 servlet 依賴. -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- 添加 JSTL(JSP Standard Tag Library,JSP標准標簽庫) -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
        </dependency>
                <!-- Jasper是tomcat中使用的JSP引擎,運用tomcat-embed-jasper可以將項目與tomcat分開 -->
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-jasper</artifactId>
            <scope>provided</scope>
        </dependency>
        
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
#熱部署
spring.devtools.livereload.enabled=true
spring.devtools.restart.enabled=true
#服務器
server.port=8888
#bean 相關
#bean延遲啟動
spring.main.lazy-initialization=false
#安全
#spring.security.user.name=root
#spring.security.user.password=root
server.servlet.context-path = /
#優雅關閉--等待還有的連接完成,之后不再允許有新的請求,類似於一些數據庫的操作
server.shutdown=graceful
spring.lifecycle.timeout-per-shutdown-phase=20s
#設置靜態資源等
spring.mvc.static-path-pattern=/**
spring.resources.static-locations=/css/,/images/,/WEB-INF/plugin/

#啟動jsp功能
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
# 數據庫連接(mysql)
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url =jdbc:mysql://127.0.0.1:7799/spring?rewriteBatchedStatements=true&autoReconnect=true&allowMultiQueries=true&useSSL=false&serverTimezone=CST&allowPublicKeyRetrieval=true
spring.datasource.username =lzf
spring.datasource.password =123

#連接池配置-HikariCp-----------------------------------------------------
#鑒於springboot目前的版本,spring.datasource.type也可以不寫
spring.datasource.name=hcmdmserverDs
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
#是否自動提交,默認true
spring.datasource.hikari.autoCommit=false
#連接超時,過了這個時間還連接不到,hikari會返回錯誤,設置3分鍾
spring.datasource.hikari.connectionTimeout=180000
#多了多少毫秒不用,會被設置為空閑,默認是10分鍾
spring.datasource.hikari.idleTimeout=600000
#最大生命周期,默認1800000(30分鍾)
spring.datasource.hikari.maxLifetime=1800000
#最少空閑連接
spring.datasource.hikari.minimumIdle=3
#最大連接池大小=空閑+在用
spring.datasource.hikari.maximumPoolSize=6
spring.datasource.hikari.connection-test-query=select 1
#如果不指定 spring.datasource.type,則以下可以是通用的連接池配置信息

#spring-jdbc

#http-請求連接池

 

 

二、org.springframework.security.core.userdetails.UserDetailsService實現類

這個部分的關鍵是兩點:

1)實現UserDetailsService的時候,返回一個UserDetails即可

2)用戶密碼不需要像一些地方說的那樣要有{bcrypt}之類的前綴

/**
 * 
 * @author     lzfto
 * @apiNote   
 */
@Service
public class UdsDetail implements UserDetailsService {

    @Autowired
    MyUserService usersService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserRolePojo ur = usersService.getUserRoleDetail(username);
        return change(ur);
    }

    private UserDetails change(UserRolePojo ur) {
        List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();
        List<RolePojo> roleList = ur.getRoleList();
        for (RolePojo role : roleList) {
            GrantedAuthority gt = new SimpleGrantedAuthority(role.getName());
            authorityList.add(gt);
        }
        UserDetails user = new User(ur.getUserPojo().getName(), ur.getUserPojo().getPassword(), authorityList);
        return user;
    }
}

至於如何和數據庫關聯,還是比較簡單的,不需贅述!

此處附上插入用戶信息的腳本:

-- 學生表
CREATE TABLE `users` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(40) NOT NULL,
  `password` varchar(70) NOT NULL,
  `create_time` varchar(20) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
create table role(
  id int not null auto_increment,
  name varchar(40) not null,
  primary key(id)
);

create table user_role_detail(
 id int not null auto_increment,
 user_id int not null,
 role_id int not null,
 primary key(id)
);
create unique index uid_user_role_detail on user_role_detail(user_id,role_id);


--
-- 密碼是123
-- 不需要添加什么 bcrypt之類的前綴,在2.3.4版本中。
INSERT INTO `spring`.`users` (`name`, `password`, `create_time`) VALUES ('lzf', '$2a$10$Mg8XzxbqsOMQAxrPD8d9hOELzDyGc7lShVdSb7vOLWwEplWlga7cO', '2020-08-11 12:00:00');
INSERT INTO `spring`.`users` (`name`, `password`, `create_time`) VALUES ('wth', '$2a$10$Mg8XzxbqsOMQAxrPD8d9hOELzDyGc7lShVdSb7vOLWwEplWlga7cO', '2020-08-11 12:00:00');
--
INSERT INTO `spring`.`role` (`name`) VALUES ('ADMIN');
INSERT INTO `spring`.`role` (`name`) VALUES ('MANAGER');
INSERT INTO `spring`.`role` (`name`) VALUES ('LEADER');
INSERT INTO `spring`.`role` (`name`) VALUES ('CEO');
--
insert into user_role_detail(user_id,role_id) select u.id,r.id from users u,role r where u.name='lzf';
insert into user_role_detail(user_id,role_id) select u.id,r.id from users u,role r where u.name='wth' and r.name!='ADMIN';

 

 

三、WebSecurityConfigurerAdapter等有關配置

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UdsDetail uService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * 1.需要把loginPage,loginProcessingUrl放開 permiAll,否則會進入無限循環重定向
         * 2.antMatchers("/doLogin", "/touch","/error404").permitAll()的順序不重要
         * 3.無需要配置 scanBasePackages
         */
        http
        .formLogin()
        .loginPage("/doLogin")
        .loginProcessingUrl("/touch")
        .and()
        .authorizeRequests()
        .antMatchers("/doLogin", "/touch","/error404","/plugin/*").permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().disable();
    }
    
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.userDetailsService(uService).passwordEncoder(encoder);
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/plugin/**", "/css/**", "/images/**");
    }

}

第一次看別人的"configure(HttpSecurity http)"方法的時候,

總有疑問:

  •    "loginProcessingUrl"是不是一定是/login?
  • 是不是可以是其它的?
  • 就算是/login,那么是否需要在控制器中定義一個對應的方法?

明確的答案見最后一個小節:“創建login.jsp”,此處不再贅述。

 

為了讓系統找到/doLogin,必須定義一個控制器方法

    @RequestMapping("/doLogin")
    public ModelAndView skipLogin() {
        ModelAndView mv = new ModelAndView("login");
        return mv;
    }

有的人不喜歡使用控制器,直接使用某個頁面替代,譬如的login.jsp。這就是存粹的個人習慣了!

 

應用啟動類:

@SpringBootApplication
public class SecutiryApplication {
    public static void main(String[] args) {
        SpringApplication.run(SecutiryApplication.class, args);
    }

}

 SpringBootApplication無需具有 scanBasePackages的語法,因為那樣會導致springboot使用默認的WebSecurityConfigurerAdapter 覆蓋用戶自己自定義的類,譬如上文的WebSecurityConfig

 

四、模板引擎,個人推薦使用jsp

為什么使用springboot+jsp,是因為springboot搭建mvc的確方便,其次jsp是公司大部分人都會,都熟悉的語言,而thymeleaf之類的模板引擎

額外增加了學習成本,但我們的項目並沒有那么cloud。

所以綜合起來,使用springboot+jsp是不錯的

 

五、目錄結構

在src/main/下創建:

/src/main/webapp/WEB-INF

/src/main/webapp/WEB-INF/jsp   (放jsp文件)

/src/main/webapp/plugin     (放jsp第三方插件,譬如jquery,vue,bootstrap等)

/src/main/webapp/css        (放公共樣式)

/src/main/webapp/resource  (放圖片等資源)

 

 

六、創建登錄頁面login.jsp(放在/src/main/webapp/WEB-INF/jsp下面)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html >
<head>
    <meta charset="UTF-8">
    <title>登錄測試頁面(jsp)</title>
</head>

<body>
    <h2>自定義登錄表單(jsp)</h2>
   <div>   
        <br />
        <!-- action 叫什么無所謂,只需要和 WebSecurityConfigurerAdapter 中的 loginProcessingUrl 值一致即可  -->
        <form method="post" action="/touch">
            用戶名:
            <input type="text"  id="username" name="username" placeholder="name"><br />
            密碼:
            <input type="password"  id="password" name="password" placeholder="password"><br />
            <input type="button"   value="提交"  onclick="fnLogin()">
            <!-- <input type="submit"  value="提交" > -->
        </form>
        
  </div>   
</body>
<script type="text/javascript" src="/plugin/jquery-3.4.1.min.js"></script>
<script>
function fnLogin(){
    let uName=$("#username").val();
    let pwd=$("#password").val();
    $.ajax({
        method: "post",
        url: "/login",
        cache: false,
        async: false,
        data: {
            "username":uName,
            "password":pwd
        },        
        success: function (data) {       
                //location.href ="/test/main";
                alert("good");
        },
        error: function (data) {
            console.log(data);
        }
    });
}
</script>

</html>

如果想使用form提交,那么,提交按鈕如下設置:

<!-- <input type="button" value="提交" onclick="fnLogin()"> --> <input type="submit" value="提交" >

反之,如果想使用ajax請求,那么對上面的語句反向注釋即可:

 <input type="button" value="提交" onclick="fnLogin()"> <!-- <input type="submit" value="提交" > -->

使用ajax請求的關鍵在於設定參數和url。
注:如果需要這么使用,必須保證先繼承UsernamePasswordAuthenticationFilter或者那個抽象父類AbstractAuthenticationProcessingFilter ,改寫有關內容。
如果不是很有必要,就還是老老實實使用默認的form提交。



而url在沒有修改的情況下是默認指定為/login,這是 在 UsernamePasswordAuthenticationFilter已經定義了,如下文:
/*
 * @author Ben Alex
 * @author Colin Sampaleanu
 * @author Luke Taylor
 * @since 3.0
 */
public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {
    // ~ Static fields/initializers
    // =====================================================================================

    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true;

    // ~ Constructors
    // ===================================================================================================

    public UsernamePasswordAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login", "POST"));
    }
那么是否可以不用/login? 答案是可以的! 
怎么做? 繼承AbstractAuthenticationProcessingFilter ,然后在自定義的WebSecurityConfigurerAdapter 中配置一個新的過濾器。
假定這個繼承的過濾器叫MyAuthFilter,那么MyAuthFilter在完成驗證之后,就直接繞過原有的UsernamePasswordAuthenticationFilter等后續驗證。
當然這樣的做法並不是太好!
但這是為了告訴我們驗證url是可以修改的,而不必都是/login。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM