1. 程式人生 > >十二、SpringBoot 優雅的整合Spring Security

十二、SpringBoot 優雅的整合Spring Security

前言

至於什麼是Spring security ,主要兩個作用,使用者認證和授權。即我們常說的,使用者只有登入了才能進行其他操作,沒有登入的話就重定向到登入介面。有的使用者有許可權執行某一操作,而有的使用者不能執行則是授權。算是一個專案安全框架。和shiro 框架一樣。二者的不同大家可以百度小。Spring security 是Spring家族的一員,所以Springboot算是對Spring security 進行的天然的支援。

之所以這樣說,spring security 被人詬病的配置繁瑣複雜,在springboot中變的簡單起來。如果我們只是demo 效果,可以做到0配置實現。

下面我們就一起來見識一下吧

依賴

我們在pom.xml 檔案中引入Spring security 的statter

<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>

測試

我們先來0配置的看看。引入依賴以後,我們建立一個HelloController 內容如下:

@RestController
public class HelloController {

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


然後我們啟動專案,按照我們正常的理解,直接訪問

localhost:8080/hello 

會返回hello world 。但結果卻是重定向到了/login 。下面的介面是Spring security 自帶的。


其實上面可以看到,Spring security 已經起作用了,沒有登入不能訪問 /hello 介面。

預設的使用者名稱為 user;
密碼在我們專案啟動時控制檯有列印,每次都會不一樣,隨機生成的。

我們輸入賬號密碼,再試試

可以看到,在登入之後,我們在請求 /hello 會直接返回hello world , 那是不是隻要登入一次,後面就可以一直訪問呢?當然不是的,登入成功之後,會將資訊儲存在session 中,再登入的時候,就會通過session 校驗,這樣就可以訪問到了,當session過期獲取我們手動清理掉後,就需要重新登入了。我們來試試。開啟控制檯,application 中的cookies 中的jsessionid 清理掉。

我們接著請求試試,可以發現刪除後,就會重新回到登入介面。

簡單配置使用者和密碼

上面我們使用的預設的使用者名稱和密碼,但是實際上我們肯定不會這麼做的,上面只是說明springboot 完全的集成了Spring security 。下面我們先來簡單的配置使用者名稱密碼,之所以這樣說,因為我們實際過程中應該還是不會這麼用的。之所以要講,讓大家瞭解的更全面,也為下面鋪墊。

application.properties 中配置

首先我們來簡單的,我們可以直接在application.properties 中配置使用者名稱和密碼。來代替預設使用者名稱和密碼的效果。

spring.security.user.name=quellanan
spring.security.user.password=123456
spring.security.user.roles=admin

分別是設定使用者名稱,密碼,角色。我們這裡暫時只用了使用者認證,所以角色設不設定無所謂。配置好這些之後我們重啟專案在介面上試試再。

沒有問題,但是沒有什麼用,我們實際中是不會這麼幹的吧。

記憶體中配置

在記憶體中配置的話,相對來說要複雜點,我們建立一個config 包,在包下建立SecurityConfig 類繼承 WebSecurityConfigurerAdapter


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(passwordEncoder()) // 指定加密方式
                .withUser("qaz").password(passwordEncoder().encode("123456")).roles("admin")
                .and()
                .withUser("test").password(passwordEncoder().encode("123456")).roles("USER");
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCryptPasswordEncoder:Spring Security 提供的加密工具
        return new BCryptPasswordEncoder();
    }
}

這裡我們重寫了configure(AuthenticationManagerBuilder auth) 方法,就是將定義的使用者配置到記憶體中。這裡有一個問題需要說明一下,就是這裡配置的話,密碼需要用BCryptPasswordEncoder 加密。如果不加密的話,專案編譯啟動不會報錯,但是登陸的時候就會提示賬號密碼錯誤。
還有一個問題就是,如果我們在這配置了,那我們在application.peoperties 中配置的就會失效。

上面說的這兩種方法,其實都是不常用的,我們在實際專案中根本不會在專案中寫死使用者資訊的。基本上都是存在資料庫中。所以下面我們就開始講解我們最常用的模式吧。

由於這一類,涉及的較多,就單獨一級標題出來,不放在二級標題裡面了。

從資料庫進行使用者認證

既然是用到資料庫,專案中自然要引入資料的配置啦,我這裡用的是mysql 和mybatis.
這是整個專案成型後的目錄結構,先放出來,大家心裡有底,然後一步一步的來。

建庫建表

簡單的三張表,user,roles,roles_user 。

下面是 sql。直接執行就可以

/*
Date: 2017-12-26 18:36:12
*/

CREATE DATABASE `quellanan` DEFAULT CHARACTER SET utf8;

USE `quellanan`;

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for roles
-- ----------------------------
DROP TABLE IF EXISTS `roles`;
CREATE TABLE `roles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of roles
-- ----------------------------
INSERT INTO `roles` VALUES ('1', '超級管理員');
INSERT INTO `roles` VALUES ('2', '普通使用者');
INSERT INTO `roles` VALUES ('3', '測試角色1');
INSERT INTO `roles` VALUES ('4', '測試角色2');
INSERT INTO `roles` VALUES ('5', '測試角色3');

-- ----------------------------
-- Table structure for roles_user
-- ----------------------------
DROP TABLE IF EXISTS `roles_user`;
CREATE TABLE `roles_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rid` int(11) DEFAULT '2',
  `uid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `rid` (`rid`),
  KEY `roles_user_ibfk_2` (`uid`),
  CONSTRAINT `roles_user_ibfk_1` FOREIGN KEY (`rid`) REFERENCES `roles` (`id`),
  CONSTRAINT `roles_user_ibfk_2` FOREIGN KEY (`uid`) REFERENCES `user` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=131 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of roles_user
-- ----------------------------
INSERT INTO `roles_user` VALUES ('1', '1', '1');
INSERT INTO `roles_user` VALUES ('2', '2', '2');
INSERT INTO `roles_user` VALUES ('3', '3', '3');
INSERT INTO `roles_user` VALUES ('4', '1', '4');


-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(64) DEFAULT NULL,
  `nickname` varchar(64) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT '1',
  `email` varchar(64) DEFAULT NULL,
  `userface` varchar(255) DEFAULT NULL,
  `regTime` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'quellanan', '', '$2a$10$Hv0YGLi/siOswCTP236MtOTWbClcM6rN1LCyqwfRmrwCJZqXHsj5a', '1', '[email protected]','', '2017-12-08 09:30:22');

INSERT INTO `user` VALUES ('2', 'qaz', '', '$2a$10$6H69XLebCrGhHeHzDXEoH.0x8tMFS0XfdDPwI5s.Eu9pbqRpncA.G', '1', '[email protected]','', '2017-12-08 09:30:22');

INSERT INTO `user` VALUES ('3', 'wsx', '', '$2a$10$6H69XLebCrGhHeHzDXEoH.0x8tMFS0XfdDPwI5s.Eu9pbqRpncA.G', '1', '[email protected]','', '2017-12-08 09:30:22');

INSERT INTO `user` VALUES ('4', 'test', '', '$2a$10$6H69XLebCrGhHeHzDXEoH.0x8tMFS0XfdDPwI5s.Eu9pbqRpncA.G', '1', '[email protected]','', '2017-12-08 09:30:22');


SET FOREIGN_KEY_CHECKS=1;

pom.xml 增加依賴

我們首先在原先pom 檔案基礎上增加,如下依賴。

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

前面三個是mysql 和mybatis的依賴。lombok 是一個工具類外掛。

同時我們需要修改一下pom 檔案中的build ,不然我們專案可能會找不到mybatis 的xml檔案。

    <build>
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
        </resources>

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

配置application.properties

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/quellanan?allowMultiQueries=true&useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.max-idle=10
spring.datasource.max-wait=10000
spring.datasource.min-idle=5
spring.datasource.initial-size=5

這裡如果想要列印mybatis 的sql 日誌。可以新增一個mybatis-config.xml檔案,和application.properties 同目錄

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
</configuration>

並在application.properties 中加上

mybatis.config-location=classpath:/mybatis-config.xml

entry

我們在entry 包下建立 RoleEntry。程式碼如下:

@Getter
@Setter
public class RoleEntry {
    private Long id;
    private String name;
}

我們在建立 UserEntry ,但是UserEntry 比較特殊,因為我們需要使用Spring security 。所以這裡,UserEntry 需要實現 UserDetails。
程式碼如下:

@Setter
@Getter
public class UserEntry implements UserDetails {

    private Long id;
    private String username;
    private String password;
    private String nickname;
    private boolean enabled;
    private List<RoleEntry> roles;
    private String email;
    private String userface;
    private Timestamp regTime;

    /**
     * 獲取角色許可權
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (RoleEntry role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        }
        return authorities;
    }

    /**
     * 獲取密碼
     * @return
     */
    @Override
    public String getPassword() {
        return password;
    }

    /**
     * 獲取使用者名稱
     * @return
     */
    @Override
    public String getUsername() {
        return username;
    }



    /**
     * 使用者賬號是否過期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 使用者賬號是否被鎖定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 使用者密碼是否過期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 使用者是否可用
     */
    @Override
    public boolean isEnabled() {
        return enabled;
    }
}

可以看到,基本上都是重寫的方法。也比較簡單。

mapper

這裡我將xml 檔案和介面放在一起了,你們也可以在resources 中建立一個mapper,將xml 檔案放在哪裡。
mapper層沒有什麼好說的,是mybatis 的一些知識,我們這裡講程式碼貼出來。

RolesMapper

@Mapper
public interface RolesMapper {
    List<RoleEntry> getRolesByUid(Long uid);
}

RolesMapper.xml

<?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.zlflovemm.security.mapper.RolesMapper">
    <select id="getRolesByUid" parameterType="long" resultType="com.zlflovemm.security.entry.RoleEntry">
        SELECT r.* FROM roles r,roles_user ru WHERE r.`id`=ru.`rid` AND ru.`uid`=#{uid}
    </select>
</mapper>

UserMapper

@Mapper
public interface UserMapper {
    UserEntry loadUserByUsername(@Param("username") String username);
}

UserMapper.xml

<?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.zlflovemm.security.mapper.UserMapper">
    <select id="loadUserByUsername" resultType="com.zlflovemm.security.entry.UserEntry">
        SELECT * FROM user WHERE username=#{username}
    </select>
</mapper>

service

在service 層我們要注意一點,我們需要實現 UserDetailsService 介面。
我們先建立一個UserService 繼承 UserDetailsService。然後建立一個UserServiceImpl 來時實現UserService 從而達到實現UserDetailsService的目的。這樣做是為了保證專案結構的統一層次。

UserService

public interface UserService extends UserDetailsService {
}

UserServiceImpl

@Service
@Slf4j
@Transactional
public class UserServiceImpl implements UserService {
    
    @Autowired
    UserMapper userMapper;
    @Autowired
    RolesMapper rolesMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        UserEntry user = userMapper.loadUserByUsername(s);
        if (user == null) {
            //避免返回null,這裡返回一個不含有任何值的User物件,在後期的密碼比對過程中一樣會驗證失敗
            return new UserEntry();
        }
        //查詢使用者的角色資訊,並返回存入user中
        List<RoleEntry> roles = rolesMapper.getRolesByUid(user.getId());
        user.setRoles(roles);
        return user;
    }
}

可以看到,主要是為了實現 loadUserByUsername的方法。在這個方法中我們 loadUserByUsername和getRolesByUid 就是我們在mapper 定義的查詢資料庫資料的方法。

SecurityConfig

前面做了這麼多,其實都是準備工作,主要的目的就是提供一個Bean 。做完上面這些,我們再回到 SecurityConfig 中,其實我們現在需要修改的很少了。
我們將使用者寫在記憶體的方法註釋掉。通過資料庫查詢。


@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCryptPasswordEncoder:Spring Security 提供的加密工具
        return new BCryptPasswordEncoder();
    }
    
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService)
                .passwordEncoder(passwordEncoder());//passwoldEncoder是對密碼的加密處理,如果user中密碼沒有加密,則可以不加此方法。注意加密請使用security自帶的加密方式。
    }

}

可以和開始的 SecurityConfig 檔案對比下,其實你就是多了一個userService,然後configure(AuthenticationManagerBuilder auth)中是通過userService 進行校驗的。

測試

好了,其實到這裡,我們就已經完成了,我們啟動專案,就可以看到和之前寫在記憶體中達到一樣的效果。

過濾

以為到這就完了,其實還有一點哈哈。我們現在是所有的介面都需要先登入才能訪問,沒有登入的話就跳轉到login介面。實際上我們肯定有些是不需要認證也可以訪問的,比如以下靜態檔案或者註冊的請求。
所以我們還是要配置一下過濾。

其實也很簡單,一樣的在 SecurityConfig 檔案中 重寫 configure(HttpSecurity http) 方法。
這裡我直接參考官網上的。
https://spring.io/guides/gs/securing-web/

該configure(HttpSecurity)方法定義應保護哪些URL路徑,不應該保護哪些URL路徑。具體而言,“ /”和“ / home”路徑配置為不需要任何身份驗證。所有其他路徑必須經過驗證。
使用者成功登入後,他們將被重定向到之前要求身份驗證的頁面。有一個由指定的自定義“ /登入”頁面loginPage(),每個人都可以檢視它。

我們程式碼中 把 loginPage("/login") 註釋掉就好了,如果不註釋的話,就需要我們自己寫login 介面和請求。我們這裡就用框架自帶的。

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/", "/hello").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            //.loginPage("/login")
            .permitAll()
            .and()
            .logout()
            .permitAll();
    }

這樣配置就說明 /hell 和 / 請求不會攔截,其他的請求,需要先登入才能訪問。
為了更方便的看到效果,我們在HelloController 中再加兩個方法

    @RequestMapping("/hello2")
    public String hello2(){
        return "hello adada";
    }

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

現在我們啟動來看下效果。

證明我們配置的過濾是有效果的。

番外

到此算是差不多結束了,其實還有很多知識點,不是一篇文章能講完的,這裡算是拋轉引玉,希望對大家有幫助。後面我也會持續更新

好了,原始碼我上傳到github 上啦
https://github.com/QuellanAn/security

後續加油♡

歡迎大家關注個人公眾號 "程式設計師愛酸奶"

分享各種學習資料,包含java,linux,大資料等。資料包含視訊文件以及原始碼,同時分享本人及投遞的優質技術博文。

如果大家喜歡記得關注和分享喲❤

相關推薦

SpringBoot 優雅整合Spring Security

前言 至於什麼是Spring security ,主要兩個作用,使用者認證和授權。即我們常說的,使用者只有登入了才能進行其他操作,沒有登入的話就重定向到登入介面。有的使用者有許可權執行某一操作,而有的使用者不能執行則是授權。算是一個專案安全框架。和shiro 框架一樣。二者的不同大家可以百度小。Spring

十三springboot 優雅整合spring-boot-admin 實現程式監控

前言 我們知道專案的監控是尤為重要的,但是我們如果用jdk 自帶的jconsole 和jvisualvm 的話會非常繁瑣,且介面不是很友好。之前我們使用了spring boot 專案,但是都沒有對專案有一個很好的監控。在spring 家族中有 spring-boot-admin 可以很好的幫我們起到監控微服務

springboot 整合Junit

一、maven依賴 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test&l

springboot系列springboot整合RestTemplateswaggerUI

一、背景介紹  在微服務都是以HTTP介面的形式暴露自身服務的,因此在呼叫遠端服務時就必須使用HTTP客戶端。我們可以使用JDK原生的URLConnection、Apache的Http Client、Netty的非同步HTTP Client, Spring的RestTemplate。這裡介紹的是RestTe

Springboot 配置

(一)配置的作用   Spring Boot 應用的外部配置資源,這些配置資源能夠與程式碼相互配合,避免硬編碼 方式,提供應用資料或行為變化的靈活性。 (二)型別 Properties 檔案 YAML 檔案 環境變數 Java 系統屬性 命令列 (三)載入順

SpringBoot配置Filter以及註解配置CAS客戶端過濾器

spring boot 配置Filter過濾器 參考: CAS單點登入詳解  CAS單點登入疑問解答  Filter過濾器,Interceptor攔截器,ControllerAdvice,Aspect切片 1、通過 @WebFilter 註解來配置

Springboot整合kafka

(一)新增依賴 <dependency> <groupId>org.springframework.kafka</groupId> <artifactId>spring-kafka</a

SpringBoot與分散式(Zookeeper和DubboSpring Boot和Spring Cloud)

一、分散式應用 在分散式系統中,國內常用zookeeper+dubbo組合,而SpringBoot推薦使用全棧的Spring,SpringBoot+SpringCloud。 分散式系統: 單一應用架構 當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減

Spring Boot整合的運維管理工具

(一)新增依賴 <!--運維管理工具依賴--> <dependency> <groupId>org.springframework.boot</groupId>

Springboot整合Web services,構建SOAP服務

(一)新增依賴 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-we

springboot 整合spring security

轉載務必說明出處:https://blog.csdn.net/LiaoHongHB/article/details/83576911        spring security 是一種安全框架,主要的作用是認證和授權;其中認證則是判斷該使用者是誰,授權則

SpringBoot整合Spring Security(1)——入門程式

因為專案需要,第一次接觸Spring Security,早就聽聞Spring Security強大但上手困難,今天學習了一天,翻遍了全網資料,才僅僅出入門道,特整理這篇文章來讓後來者少踩一點坑(本文附帶例項程式,請放心食用) 本篇文章環境:SpringBoot 2.0 + Mybatis + S

SpringCloud之Spring Cloud Bus配置中心

一、簡介 ConfigServer使用了Spring Cloud Bus之後(引入Spring Cloud Bus用來操作訊息佇列),會對外提供一個介面,叫做bus-refresh,遠端git訪問這個介面ConfigServer就會把配置更新的資訊傳送到訊息佇列(RabbitMQ)裡面,Co

SpringBoot整合Spring-Security

1、新增依賴 // 新增spring security依賴 compile('org.springframework.boot:spring-boot-starter-security') //

SpringBoot之資料訪問整合SpringData JPA

1.SpringData簡介 2.整合SpringData JPA JPA:ORM(Object Relational Mapping 物件關係對映); 1.編寫一個實體類(bean)和資料表

SpringBoot之資料訪問整合MyBatis

pom.xml <!--引入mybatis--> <dependency> <groupId>org.myba

SpringBoot 2.0.5簡單整合Spring Security遇到的坑

SpringBoot整合Security的部落格案例網上已經很多了,但個人覺得對於一個初次整合Security的同學來說,一個簡單的案例還是很有必要的。為此,上傳一個本人整合的案例,僅供大家參考,也為自己記錄一下,話不多說,表演開始。 版本介紹:SpringBoot 2.0

SpringBoot之RabbitMQ

一、RabbitMQ核心概念 RabbitMQ是一個由erlang開發的AMQP(Advanved Message Queue Protocol)的開源實現。 Message 訊息,訊息是不具名的,它由訊息頭和訊息體組成。訊息體是不透明的,而訊息頭則由一系列的可選屬性組成

Spring Security):6.4 Method Security

From version 2.0 onwards Spring Security has improved support substantially for adding security to your service layer methods. It provides support for JSR-

SpringBoot 整合 swagger2 (swagger2 版本 2.8.0)

(一)新增依賴 <swagger.version>2.8.0</swagger.version> <!-- swagger2 restful api 文件 start --> <dependency> <gro