Spring Boot整合Spring Security自定義登錄實戰


本文主要介紹在Spring Boot中整合Spring Security,對於Spring Boot配置及使用不做過多介紹,還不了解的同學可以先學習下Spring Boot。

本demo所用Spring Boot版本為2.1.4.RELEASE。

1、pom.xml中增加依賴

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

2、Spring Security配置類

package com.inspur.webframe.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

import com.inspur.webframe.security.UserDetailsServiceImpl;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    @Override
    protected UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .authorizeRequests().anyRequest().authenticated()
        .and()
            .csrf().disable() //禁用csrf
            .headers().frameOptions().disable() //禁用frame options
        .and()
            .formLogin().loginPage("/demo/login").loginProcessingUrl("/j_spring_security_check").failureUrl("/demo/login?error=true").defaultSuccessUrl("/demo/main").permitAll()
        .and()
            .logout().logoutUrl("/j_spring_security_logout").logoutSuccessUrl("/demo/login").permitAll();
    }
}

 userDetailsService返回自己實現的UserDetailsService,見下面UserDetailsServiceImpl類。

configure方法中配置了如下內容:

  登錄頁面url:/demo/login

  登錄處理url:/j_spring_security_check,對應登錄頁面中登錄操作url

  登錄失敗url:/demo/login?error=true

  登錄成功url:/demo/main

  注銷url:/j_spring_security_logout,對應歡迎頁面中注銷操作url

  注銷成功跳轉url:/demo/login,調到登錄頁面

3、用戶類

該類與數據庫的用戶表對應

package com.inspur.webframe.security;

import java.io.Serializable;

import com.inspur.common.entity.BaseEntity;

public class User extends BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 用戶id
     */
    private String userid;
    
    /**
     * 用戶密碼
     */
    private String password;
    
    /**
     * 用戶名
     */
    private String username;
    
    /**
     * 是否被鎖定 1:是 0:否
     */
    private Integer isLocked;
    
    public Integer getIsLocked() {
        return isLocked;
    }
    public void setIsLocked(Integer isLocked) {
        this.isLocked = isLocked;
    }
    public String getUserid() {
        return userid;
    }
    public void setUserid(String userid) {
        this.userid = userid;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    
    @Override
    public String toString() {
        return "User [userid=" + userid + ", password=" + password + ", username=" + username + "]";
    }
}

BaseEntity是一個基類,有id、創建時間、修改時間等基礎信息,作為demo可以忽略

4、自定義UserDetails

該類需要實現org.springframework.security.core.userdetails.UserDetails接口,作為用戶信息;該類關聯用戶類

package com.inspur.webframe.security;

import java.util.Collection;

import org.apache.commons.codec.binary.Base64;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.inspur.common.util.EncoderUtil;

public class SecurityUser implements UserDetails {
    private static final long serialVersionUID = 4118167338060103803L;
    private User systemUser = null;
    private Collection<? extends GrantedAuthority> authorities = null;
    
    public SecurityUser(User systemUser, Collection<? extends GrantedAuthority> authorities) {
        this.systemUser = systemUser;
        this.authorities = authorities;
    }
    
    public User getSystemUser() {
        return systemUser;
    }

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

    @Override
    public String getPassword() {
     //{MD5}e10adc3949ba59abbe56e057f20f883e,123456
   return systemUser.getPassword(); } @Override public String getUsername() { return systemUser.getUserid(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return !(systemUser.getIsLocked() != null && systemUser.getIsLocked() == 1); } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }

 spring security 有多種驗證密碼算法,這里使用的MD5算法,格式為:{MD5}e10adc3949ba59abbe56e057f20f883e;如果數據保存的密碼格式不是這種格式,可以在getPassword()方法中轉換成標准格式。

5、自定義UserDetailsService

該類需要實現org.springframework.security.core.userdetails.UserDetailsService接口,用於用戶的登錄認證

package com.inspur.webframe.security;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

import com.inspur.common.dao.BaseDao;

public class UserDetailsServiceImpl implements UserDetailsService {
    protected static Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
    
    @Autowired
    private BaseDao baseDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info(username);
        User user = baseDao.selectForObject(User.class, "userid=?", username);
        logger.info("user={}", user);
        
        if (user != null) {
            //權限,應從數據取這里寫死
            List<GrantedAuthority> authorities= new ArrayList<GrantedAuthority>();
            authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            SecurityUser u = new SecurityUser(user, authorities);
            
            logger.info(u.getPassword());
            return u;
        }
        throw new UsernameNotFoundException("用戶(" + username + ")不存在");
    }
}

baseDao是我實現的操作數據庫的工具類,類似spring的jdbcTemplate;不是重點,具體實現細節就不貼出來了,看代碼也能看出意思

6、訪問url的controller

package com.inspur.demo.web.controller;

import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.inspur.common.web.controller.BaseController;

@Controller
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
public class DemoController extends BaseController {
    
    @RequestMapping(value={"/demo/login", "/"})
    public String login() {
        return "/demo/login";
    }
   
    @RequestMapping(value={"/demo/main"})
    public String main(Authentication authentication) {
        logger.info("authentication.getPrincipal()={}", authentication.getPrincipal());
        return "/demo/main";
    }
    
}

7、thymeleaf

thymeleaf是spring推薦使用的模板引擎,可以優雅的來畫頁面。

  • 引入依賴
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 配置application.yml(或application.properties)
spring:
  resources:
    static-locations: classpath:/static/
  thymeleaf:
    encoding: utf-8
    cache: false

8、編寫頁面login.html

頁面位置為/src/main/resources/templates/demo/login.html

<!DOCTYPE HTML>
<html>
  <head>
    <title>My JSP 'login.jsp' starting page</title>
    
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    

  </head>
  
  <body>
    <form th:action="@{/j_spring_security_check}" method="post">
    <input type="hidden"  name ="${_csrf.parameterName}"  value ="${_csrf.token}" /> 
      <table>
         <tr>
            <td> 用戶名:</td>
            <td><input type="text" name="username"/></td>
         </tr>
         <tr>
            <td> 密碼:</td>
            <td><input type="password" name="password"/></td>
         </tr>
         <tr>
            <td>
                <span style="color: red;" th:if="${param.error != null && session.SPRING_SECURITY_LAST_EXCEPTION != null }" th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}"></span>
            </td>
         </tr>
         <tr>
            <td colspan="2" align="center">
                <input type="submit" value=" 登錄 "/>
                <input type="reset" value=" 重置 "/>
            </td>
         </tr>
      </table>
   </form>
  </body>
</html>

/j_spring_security_check對應上面SecurityConfig配置的登錄路徑;session.SPRING_SECURITY_LAST_EXCEPTION.message表示登錄錯誤的信息。

9、編寫頁面main.html

頁面位置為/src/main/resources/templates/demo/main.html

<!DOCTYPE HTML>
<html>
  <head>
    <title>My JSP 'main.jsp' starting page</title>
    
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    

  </head>
  
  <body>
  歡迎! <a th:href="@{/j_spring_security_logout}">退出</a>
  </body>
</html>

/j_spring_security_check對應上面SecurityConfig配置的注銷路徑

10、測試

訪問登錄頁面http://localhost:8080/webframe/demo/login,我的server.servlet.context-path配置為/webframe

  登錄失敗:

  登錄成功:

11、擴展功能-鎖定用戶

簡單實現:用戶表中需要有is_locked(是否鎖定)、login_fail_times(連續登錄失敗次數)這兩個字段;連續登錄失敗次數超過一定值就鎖定用戶。

增加listener監聽登錄事件。

package com.inspur.webframe.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.stereotype.Component;

import com.inspur.common.dao.BaseDao;

@Component
public class LoginListener implements ApplicationListener<AbstractAuthenticationEvent> {
    private static Logger logger = LoggerFactory.getLogger(LoginListener.class);
    private static int MAX_FAIL_TIMES = 3;
    @Autowired
    private BaseDao baseDao;
    
    @Override
    public void onApplicationEvent(AbstractAuthenticationEvent event) {
        logger.info(event.getClass().toString());
        String userId = event.getAuthentication().getName();
        if (event instanceof AuthenticationSuccessEvent) {
            baseDao.update("update a_hr_userinfo set login_fail_times=0 where userid=? and login_fail_times>0", userId);
        } else if (event instanceof AuthenticationFailureBadCredentialsEvent) {
            baseDao.update("update a_hr_userinfo set login_fail_times=login_fail_times+1 where userid=?", userId);
            baseDao.update("update a_hr_userinfo set is_locked=1 where userid=? and login_fail_times>=?", userId, MAX_FAIL_TIMES);
        }
    }
}

登錄成功login_fail_times清0,登錄失敗login_fail_times加1,到達3就鎖定用戶;這邊也用到了baseDao,具體意思看代碼也能明白。


免責聲明!

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



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