天天看點

springsecurity權限驗證

現在是大三暑假,有機會來到企業裡面學習一些東西,在接觸一段 spring boot 之後需要做權限驗證,隻是對登入使用者的進行權限驗證,将整理的内容和大家分享一下。

看到的一些部落格的内容:

https://blog.csdn.net/UpdateAllTheTime/article/details/82664103

https://www.cnblogs.com/rolandlee/p/9580492.html

上面的都不是 springboot.security 的内容,而是控制 mvc 的方式。

下面的是使用 springboot.security 進行的配置,這個是 2.0

https://blog.csdn.net/qq_24434671/article/details/86595157

https://blog.csdn.net/u014174854/article/details/83338804

大佬:https://www.iteye.com/blog/412887952-qq-com-2441544

最詳細的資訊自然:來自官方文檔https://spring.io/blog/2013/07/03/spring-security-java-config-preview-web-security/#wsca

上面的部落格,有的寫的偏理論,實踐性的内容涉及的内容不适合我現在的知識架構,有的是完全沒有注釋,真的讓人很苦惱,下面把搜集到的資訊進行整理,得到一份比較實用的内容。

首先,我們進行權限管理使用到的是 spring 架構中的 SpringSecurity 部分的内容。

什麼是 SpringSecurity?

是基于 SpringAOP 和 Servlet 過濾的安全架構,在 Web 請求級和方法調用級處理身份确認和授權,還是使用的還是依賴注入和面向切面的功能。

安全主要包括兩個操作 ”認證“ (為使用者建立一個他所聲明的主體,主體一般指的是使用者、裝置、系統)和 ”授權“ (或者權限控制)。

其中 Spring Security 對 Web 安全性的支援大量地依賴于 Servlet 過濾器。這些過濾器攔截進入的請求,在應用程式處理該請求之前進行某些安全處理。

FilterToBeanProxy 是一個特殊的 Servlet 處理器,它本身沒有做太多的工作,而是将自己的工作委托給 Spring 應用程式上下文中的一個 Bean 完成。被委托的 Bean 和其他的 Servlet 過濾器一樣,實作 javax.servlet.Filter 的接口。FilterToBeanProxy代理給的那個Bean可以是javax.servlet.Filter的任意實作。

需要添加的依賴

web 啟動器和 security 啟動器

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

資料庫的設計:

想要達到的目的是:

對不同身份的使用者添加限定,指定身份的使用者,隻能進行指定的操作。

涉及的表:使用者表(user),權限表(role),權限配置設定表(user_role)

各個表的内容:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `password` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `gender` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci 
           
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `rolename` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `roledesc` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

           
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `userId` int(11) NOT NULL,
  `roleId` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `userId` (`userId`),
  KEY `roleId` (`roleId`),
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`userId`) REFERENCES `user` (`id`),
  CONSTRAINT `user_role_ibfk_2` FOREIGN KEY (`roleId`) REFERENCES `role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

           

想要查詢所用使用者對應的權限資訊使用到的 sql 語句

select u.username as name, u.password as password, r.rolename as role, r.roledesc as describute
 from user as u, role as r, user_role as ur
 where u.id=ur.userId and ur.roleId = r.id;
           

為了友善,可以用上面的 sql 語句建立了視圖:

+------------+-------------+------+-----+---------+-------+
| Field      | Type        | Null | Key | Default | Extra |
+------------+-------------+------+-----+---------+-------+
| name       | varchar(20) | YES  |     | NULL    |       |
| password   | varchar(20) | YES  |     | NULL    |       |
| role       | varchar(20) | YES  |     | NULL    |       |
| describute | varchar(50) | YES  |     | NULL    |       |
+------------+-------------+------+-----+---------+-------+
           

插入的資料在視圖中顯示如下為:

+-------+----------+--------+------------+
| name  | password | role   | describute |
+-------+----------+--------+------------+
| admin | admin    | admin  | admin      |
| admin | admin    | normal | normal     |
| user  | 123      | admin  | admin      |
+-------+----------+--------+------------+
           

建立 springboot 的項目

完整的 pom.xml 檔案,關于mybayis 部分需要導入兩個依賴。

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.nianzuochen.cn</groupId>
    <artifactId>testsecurity</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <!-- springsecurity 啟動器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- spring 的 web 啟動器 -->
        <!-- 不使用内置的 tomcat 改為内資的 undertow -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-tomcat</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>

        <!-- 簡化實體類方法書寫 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.22</version>
            <scope>provided</scope>
        </dependency>

        <!-- 資料庫連接配接 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.6</version>
        </dependency>

        <!--
            整合 mybatis 需要導入 mybatis-spring-boot-starter
            和 mysql-connector-java
        -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>

        <!-- swagger2 文檔自動生成做測試 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.2.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <!-- spring 提供的插件,可進行 package,clean 等 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <!-- 配置掃描器查詢到 .xml 檔案 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>false</filtering>
            </resource>
        </resources>
    </build>
</project>
           

添加了資料庫連接配接的啟動器,是以需要添加關于資料庫連接配接的配置:

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/testsecurity?characterEncoding=utf8&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
           

編寫啟動類,啟動 springboot 項目

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
@EnableSwagger2
@MapperScan("com.nianzuochen.cn.mapper")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
           

idea的控制台會出現一個初始的密碼:

springsecurity權限驗證

浏覽器輸入:http://localhost:8080 會得到一個預設的使用者登入頁面:

springsecurity權限驗證

可以在啟動類中将這個預設的登入驗證框去掉:

@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
           

接下來完成針對資料庫的 mybatis 的内容

需要注意的部分:

1.UserInfo 類:

SpringSecurity 使用的是 UserDetail 作為登陸使用者類,也就是使用者登入的 username 和 password 會被封裝進 UserDetail 類中,進行接下來的授權。

/**
 * 存儲使用者的詳細資訊,繼承自 UserDetails 類,将使用者的資訊送出給 WebSecurityConfigurerAdapter
 * web 安全配置擴充卡,通過該類中提供的資訊判斷登入使用者的權限,進行權限處理
 */

@Getter
@Setter
public class UserInfo implements UserDetails {

    private Long id;
    private String username;
    private String password;
    private Integer gender;
    private List<Role> roles;


    /**
     *  繼承的方法授予的權限的内容
     * @return
     */
    @Override
    @JsonIgnore
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            // GrantedAuthority 的實作類 SimpleGrantedAuthority
            authorities.add(new SimpleGrantedAuthority(role.getRolename()));
        }
        return authorities;
    }

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

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

    /**
     * 表明使用者的賬戶是否過期,設定為未過期,這裡可以添加自定義的過期内容
     * 比如:在使用者資訊中添加登入的時間,然後設定過期時間,判斷後決定是否讓目前登入使用者的賬戶過期
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 表明目前使用者是否被上鎖
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 判斷目前登入使用者的憑證(密碼)是否過期,過期的憑證阻止身份驗證
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 判斷目前使用者是否啟用
     * @return
     */
    @Override
    @JsonIgnore
    public boolean isEnabled() {
        return true;
    }
}
           

2.role 類:

Role 是 UserInfo 的一個參數,因為 UserInfo extends UserDetail ,而 UserDetail implements Serializable 是以 Role 也需要實作序列化。

還有在 SpringSecurity 中限定使用者的權限名稱必須以 ROLE_ 開頭,在輸入測試用例的時候要注意。

@Getter
@Setter
public class Role implements Serializable {

    private Long id;
    /**
     * rolename 的内容必須是以 ROLE_ 開頭的内容,
     */
    private String rolename;
    private String roledesc;
}
           

3.對應的 xml 檔案:

涉及到多表查詢

<mapper namespace="com.nianzuochen.cn.mapper.RoleMapper">
    <select id="loadRoleByUserId" resultType="com.nianzuochen.cn.domain.Role">
        select * from role as r, user_role as ur where #{userId} = ur.userId and ur.roleId = r.id;
    </select>
</mapper>
           
<mapper namespace="com.nianzuochen.cn.mapper.UserInfoMapper">
    <!-- 使用 mybatis 将查詢到的資料和 UserInfo 進行比對 -->
    <select id="loadUserByUsername" resultMap="lazyLoadRoles" >
        select * from user WHERE username=#{username};
    </select>
    <resultMap id="lazyLoadRoles" type="com.nianzuochen.cn.dao.UserInfo">
        <id column="id" property="id"/>
        <id column="username" property="username"/>
        <id column="password" property="password"/>
        <id column="gender" property="gender"/>
        <collection property="roles" ofType="com.nianzuochen.cn.domain.Role"
                    select="com.nianzuochen.cn.mapper.RoleMapper.loadRoleByUserId" column="id">
        </collection>
    </resultMap>
</mapper>
           

4.UserInfoService 類

SpringSecurity 在使用 UserDetail 對使用者資訊做了限定,同時對 Service 也進行了限定。給出父類 UserDetailService,裡面僅有一個根據使用者名查詢使用者資訊的抽象方法

UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
           

UserInfoServiceImpl 類自然要實作這個基礎方法:

@Service
// @Transactional 注解管理事務的
@Transactional
public class UserInfoServiceImpl implements UserDetailsService {


    @Autowired
    UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserInfo userInfo = userInfoMapper.loadUserByUsername(username);
        if (userInfo == null) {
            throw new UsernameNotFoundException("使用者名不對");
        }

        return userInfo;
    }
}
           

5. 在 controller 中添加一個測試:

使用 swagger,也就是在浏覽器中輸入 http://localhost:8080/swagger-ui.html 調出 swagger 開始測試接口文檔。

/**
 * 使用 admin 測得如下内容
 * 測試查詢到使用者的資訊,在實體類中将使用者的 username 和 password 進行了忽略處理
 * {"id":1,"gender":1,"roles":[{"id":1,"rolename":"ROLE_admin","roledesc":"admin"},{"id":2,"rolename":"ROLE_normal","roledesc":"normal"}]}
 * @return
 */
@GetMapping("/user")
public UserInfo getAdmin(String username) {
    return (UserInfo) userInfoService.loadUserByUsername(username);
}
           

目前位置資料采集的部分算是完成了,接下來的是使用 SpringSecurity 授權的内容。

springsecurity權限驗證

最基本的安全配置資訊在 WebSecurityConfigureAdapter 類中,裡面有很多安全相關的配置:

//身份認證管理器建立者
protected void configure(AuthenticationManagerBuilder auth) 
// 關于 web 安全的配置
public void configure(WebSecurity web)
// 關于 http 安全的配置
protected void configure(HttpSecurity http)
......
           

在對目前登入對象進行資料比對的時候自然要導入所有使用者的資訊,在 spring security 5.x 之後需要對使用者的密碼指定加密方式,采用 Bcypt : bcrypt 是一種跨平台的檔案加密工具。

想要開啟 Spring 方法級的安全,使用 @EnableGlobalMethodSecurity 注解

下面是此次權限驗證 WebSecurityConfigureAdapter 的完整資訊

/**
 * 此類是一個配置類
 */
@Configuration      // 聲明為配置類
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserInfoService userInfoService;

    /**
     * 為登入添加使用者權限
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        ......
        // 具體内容在下面分解
    }

    /**
     * 網絡安全,忽略一些 ant 風格的請求,使用者無法直接擷取網站的資源
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        System.out.println("websecurity");
        ......
        // 具體内容在下面分解
    }

    /**
     * 這個是主要配置,對 http 請求加限定,包括請求成功和失敗的以及退出的處理
     * 在 HttpSecurity 類的注解中詳細介紹了各種方法調用
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("HttpSecurity");
        ......
        // 具體内容在下面分解
    }
}
           

一共 override 了三個方法,三個方法的調用順序是:

添加使用者權限
HttpSecurity
websecurity
           

首先

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    System.out.println("添加使用者權限");

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

設定 AuthenticationManagerBuilder (身份認證管理器),用來查詢使用者名和密碼的處理器設定成自己的處理器方法,然後為認證配置設定加密方式。

接着

/**
 * 網絡安全,忽略一些 ant 風格的請求,使用者無法直接擷取網站的資源
 * @param web
 * @throws Exception
 */
@Override
public void configure(WebSecurity web) throws Exception {
    System.out.println("websecurity");
    web.ignoring().antMatchers("/index.html", "/static/**", "/login_p",
            "/favicon.ico","/swagger-ui.html","/webjars/**","/swagger-resources/**","/v2/**");
}
           

最後

/**
 * 這個是主要配置,對 http 請求加限定,包括請求成功和失敗的以及退出的處理
 * 在 HttpSecurity 類的注解中詳細介紹了各種方法調用
 * @param http
 * @throws Exception
 */
@Override
protected void configure(HttpSecurity http) throws Exception {
    System.out.println("HttpSecurity");

    http.
            authorizeRequests() // 定義需要保護的和不需要保護 URL
            .and()
            // 允許登入的 uri 請求
            .formLogin().loginProcessingUrl("/login")
            // 使用使用者名和密碼
            .usernameParameter("username").passwordParameter("password")
            // 登入失敗處理
            .failureHandler(new AuthenticationFailureHandler() {
                @Override
                public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");// 設定編碼格式
                    String result = "";

                    System.out.println();


                    // 對各種異常進行分類處理
                    if (e instanceof UsernameNotFoundException) {
                        result = "賬戶名不存在";
                    } else if (e instanceof LockedException) {
                        result = "登入失敗";
                    } else if (e instanceof BadCredentialsException ){
                        result = "密碼錯誤";
                    }

                    // 寫入登入失敗資訊
                    httpServletResponse.setStatus(401);
                    ObjectMapper om = new ObjectMapper();
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(om.writeValueAsString(result));
                    out.flush();
                    out.close();
                }
            })
            // 登入成功處理
            .successHandler(new AuthenticationSuccessHandler() {
                @Override
                public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");

                    // 擷取目前登入對象的主體,裡面也包含了對象的權限
                    UserInfo userInfo = (UserInfo) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                    HttpSession session = httpServletRequest.getSession();
                    session.setAttribute("operationUserId", userInfo.getId());
                    ObjectMapper om = new ObjectMapper();
                    PrintWriter out = httpServletResponse.getWriter();

                    // 登入成功将登入對象傳回
                    out.write(om.writeValueAsString(userInfo));
                    out.flush();
                    out.close();
                }
            })
            .permitAll() // 設定所有人都可以通路登位址
            .and()
            .logout()
            .logoutUrl("/logout")
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                    httpServletResponse.setContentType("application/json;charset=utf-8");
                    String result = "登出成功";
                    ObjectMapper om = new ObjectMapper();
                    PrintWriter out = httpServletResponse.getWriter();
                    out.write(om.writeValueAsString(result));
                    out.flush();
                    out.close();
                }
            })
            .permitAll()
            .and()
            .authorizeRequests()
            .antMatchers("/", "/login", "/logout").permitAll()//不攔截的位址
            .anyRequest()//剩下所有請求 進行攔截
            .authenticated();
    http.csrf().disable();//跨站請求僞造的防護,這裡關掉

}
           

這裡隻是對登入和退出進行處理,限定登入的 uri 為 login,退出的 uri 為 layout,這兩條 uri 是允許所有人進行通路的,對登入成功和登入失敗進行處理,對退出進行處理。

其中對于登入成功的處理是将使用者的詳細資訊傳遞給前端,裡面包含了使用者的權限(Role),對于不同的使用者進行區分處理的時候,傳遞的對象可能會更加的複雜,像是包含允許此類使用者進行通路的位址。

各種處理方法可以單獨的寫一個類進行傳參處理,這裡内容較小就寫在了一起。

下面是完成管理者登陸的資訊。

springsecurity權限驗證

繼續閱讀