SpringSecurity安全控件的学习

SpringSecurity简介

SpringSecurity是一个能够基于Spring的应用程序提供声明式安全保护的安全性框架,它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了SpringIOC(控制反转)和AOP(面向切面编程)功能,为应用系统提供安全访问控制功能,减少了为系统安全控制编写大量重复代码的工作。

官网地址:https://spring.io/projects/spring-security

安全框架主要包含两个操作。

认证:确认用户可以访问当前系统。

授权:确定用户在当前系统中是否能够执行某个操作,即用户所拥有的功能权限。

Security适配器

创建一个自定义类继承WebSecurityConfigurerAdapter,并在改类中使用@EnableWebSecurity注解就可以通过重写config方法类配置所需要的安全配置。

WebSecurityConfigurerAdapter是SpringSecurity为Web应用提供的一个适配器,实现了WebSecurityConfigurerAdapter接口,提供了两个方法用于重写开发者需要的安全配置。

protected void configure(HttpSecurity http) throws Exception {
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}

configure(HttpSecurity http)方法中可以通过HTTPSecurity的authorizeRequests()方法定义那些URL需要被保护、那些不需要被保护;通过formLogin()方法定义当前需要用户登录的时候,跳转到的登录页面。

configure(AuthenticationManagerBuilder auth)方法用于创建用户和用户的角色。

用户认证

SpringSecurity是通过configure(AuthenticationManagerBuilder auth)完成用户认证的。使用AuthenticationManagerBuilder的inMemoryAuthentication()方法可以添加用户,并给用户指定权限。

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().withUser("aaa").password("{noop}1234").roles("USER");
    auth.inMemoryAuthentication().withUser("admin").password("{noop}admin").roles("ADMIN", "DBA");
}

上面的代码中添加了两个用户,其中一个用户名是“aaa”,密码是“1234”,用户权限是“USER”;另一个用户名是“admin”,密码是“admin”,用户权限有两个,分别是“ADMIN”和“DBA”。需要注意的是SpringSecurity保存用户权限的时候,会默认使用“ROLE_”,也就是说“USER”实际上是“ROLE_USER”,“ADMIN”实际是“ROLE_ADMIN”,“DBA”实际上是“ROLE_DBA”。

当然,也可以查询数据库获取用户和权限。下面有写到。

用户授权

SpringSecurity是通过configure(HttpSecurity http)完成用户授权的。

HTTPSecurity的authorizeRequests()方法有多个子节点,每个macher按照它们的声明顺序执行,指定用户可以访问的多个URL模式。

  • antMatchers使用Ant风格匹配路径。
  • regexMatchers使用正则表达式匹配路径。 

在匹配了请求路径后,可以针对当前用户的信息对请求路径进行安全处理。下表是SpringSecurity提供的安全处理方法。

方法 用途
anyRequest 匹配所有请求路径
access(String) Spring EL 表达式结果为true时可以访问
anonymous() 匿名可以访问
denyAll() 用户不能访问
fullyAuthenticated() 用户完全认证可以访问(非remember-me下自动登录)
hasAnyAuthority(String...) 如果有参数,参数表示权限,则其中任何一个权限可以访问
hasAnyRole(String...) 如果有参数,参数表示角色,则其中任何一个角色可以访问
hasAuthority(String...) 如果有参数,参数表示权限,则其权限可以访问
hasIpAddress(String) 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
hasRole(String) 如果有参数,参数表示角色,则其角色可以访问
permitAll() 用户可以任意访问
rememberMe() 允许通过remember-me登录的用户访问
authenticated() 用户登录后可访问

示例代码如下:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();    //安全器令牌
    http.authorizeRequests()
            .antMatchers("/login").permitAll()
            .antMatchers("/", "/home").hasRole("USER")
            .antMatchers("/admin/**").hasAnyRole("ADMIN", "DBA")
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .usernameParameter("loginName").passwordParameter("password")
            .defaultSuccessUrl("/main")
            .failureUrl("/login?error")
            .and()
            .logout()
            .permitAll()
            .and()
            .exceptionHandling().accessDeniedPage("/accessDenied");
}
  1. http.authorizeRequests() 开始请求权限配置。
  2. antMatchers("/login").permitAll() 请求匹配“/login”,所有用户都可以访问。
  3. antMatchers("/", "/home").hasRole("USER")  请求匹配“/”和“/home”,拥有“ROLE_USER”角色的用户可以访问。
  4. antMatchers("/admin/**").hasAnyRole("ADMIN", "DBA")  请求匹配“/admin/**”,拥有“ROLE_ADMIN”或“ROLE_DBA”角色的用户可以访问。
  5. anyRequest().authenticated()  其余所有的请求都需要认证(用户登录)之后才可以访问。
  6. formLogin()  开始设置登录操作
  7. loginPage("/login")  设置登录页面的访问地址
  8. usernameParameter("loginName").passwordParameter("password")  登录时接收传递的参数“loginName”的值作为用户名,接收传递参数的“password”的值作为密码。如果不设置,默认“username”为用户名,“password”为密码
  9. defaultSuccessUrl("/main")  指定登录成功后转向的页面。
  10. failureUrl("/login?error")  指定登录失败后转向的页面和传递的参数。
  11. logout()   设置注销操作
  12. permitAll()  所有用户都可以访问。
  13. exceptionHandling().accessDeniedPage("/accessDenied")  指定异常处理页面

SpringSecurity核心类

SpringSecurity核心类包括Authentication、SecurityContextHolder、UserDetails、UserDetailsService、GrantedAuthority、DaoAuthenticationProvider和PasswordEncoder。只要掌握了这些SpringSecurity核心类,SpringSecurity就会变得非常简单。

Authentication类

Authentication用来表示用户认证信息,用户登录认证之前,SpringSecurity会将相关信息封装为一个Authentication具体实现类的对象,在登录认证成功之后又会生成一个信息更全面、包含用户权限等信息的Authentication对象,然后把它保存在SecurityContextHolder所持有的SecurityContext中,供后续的程序进行调用,如访问权限的鉴定等。

SecurityContextHolder中的getContext()方法

public static SecurityContext getContext() {
	return strategy.getContext();
}

SecurityContextHolder类

SecurityContextHolder是用来保存SecurityContext的。SecurityContext中含有当前所访问系统的用户的详细信息。默认情况下,SecurityContextHolder将使用ThreadLocal来保存SecurityContext,这也就意味着在处于同一线程的方法中,可以从ThreadLocal获取到当前的SecurityContext。

SpringSecurity使用一个Authentication对象来描述当前用户的相关信息。SecurityContextHolder中持有的是当前用户的SecurityContext,而SecurityContext持有的是代表当前用户相关信息的Authentication的引用。这个Authentication对象不需要我们自己创建,在与系统交互的过程中,SpringSecurity会自动创建相应的Authentication对象,然后赋值给当前的SecurityContext。开发过程中常常需要在程序中获取当前用户的相关信息,比如最常见的获取当前登录用户的用户名。

 String username = SecurityContextHolder.getContext().getAuthentication().getName();

 获取UserDetails类,该类中包含用户认证相关等信息。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

UserDetails类

UserDetails是SpringSecurity的一个核心接口。其中定义了一些可以获取用户名,密码、权限等与认证相关的信息的方法。SpringSecurity内部使用UserDetails实现类大都是内置的User类,要使用UserDetails,也可以直接使用该类。在SpringSecurity内部,很多需要使用用户信息的时候,基本上都是使用UserDetails,比如在登录认证的时候。

通常需要在应用中获取当前用户的其他信息,如E-mail、电话等。这时存放在Authentication中的principal只包含认证相关信息的UserDetails对象可能就不能满足我们的要求了。这时可以实现自己的UserDetails,在该实现类中可以定义一些获取用户其他信息的方法,这样将来就可以直接从当前SecurityContext的Authentication的principal中获取这些信息。

UserDetails是通过UserDetailsService的loadUserByUsername()方法进行加载的,UserDetailsService也是一个接口,我们也需要实现自己的UserDetailsService来加载自定义的UserDetails信息。

新建用户表的Service类实现UserDetailsService接口,来重写UserDetailsService的loadUserByUsername()方法,根据用户名查询当前用户信息并返回,返回的类必须继承Security内置的User类。

@Service
public class SysUserService implements UserDetailsService{
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
       
    }
}

UserDetailsService类

Authentication.getPrincipal() 的返回类型是Object。

Object getPrincipal();

但很多情况下返回的其实是一个UserDetails的实例。登录认证的时候SpringSecurity会通过UserDetailsService的loadUserByUsername()方法获取对应的UserDetails进行认证,认证通过后会将该UserDetails赋给认证通过的Authentication的Principal,然后在把该Authentication存入SecurityContext。之后如果需要使用用户信息,可以通过SecurityContextHolder获取存放在SecurityContext中的Authentication的principal,转为UserDetails类。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

GrantedAuthority类

Authentication的getAuthorities()方法可以返回当前Authentication对象用户的所有的权限。

Collection<? extends GrantedAuthority> getAuthorities();

即当前用户拥有的权限。其返回值是一个GrantedAuthority类型的数组,每一个GrantedAuthority对象代表赋予给当前用户的一种权限。GrantedAuthority是一个接口,其通常是通过UserDetailsService进行加载,然后赋予UserDetails的。

GrantedAuthority中只定义了一个getAuthority()方法,该方法返回一个字符串,表示对应的权限,如果对应权限不能用字符串表示,则返回null

获取当前用户的所有权限,存放到List集合中。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
List<String> roleCodes=new ArrayList<>();
for (GrantedAuthority authority : userDetails.getAuthorities()) {
	roleCodes.add(authority.getAuthority());
}

DaoAuthenticationProvider类

SpringSecurity默认了会使用DaoAuthenticationProvider实现AuthenticationProvider接口,专门进行用户认证的处理。DaoAuthenticationProvider在进行认证的时候需要一个UserDetailsService来获取用户的信息UserDetails,其中包括用户名,密码和所拥有的权限等。如果需要改变认证的方式,开发者可以实现自己的AuthenticationProvider。

PasswordEncoder类

在SpringSecurity中,对密码的加密都是由PasswordEncoder来完成的。在SpringSecurity中,已经对PasswordEncoder有了很多实现,包括md5加密,SHA-256加密等,开发者只需要直接拿来用就可以。在DaoAuthenticationProvider中,有一个就是PasswordEncoder熟悉,密码加密功能主义靠它来完成。

SpringSecurity的验证机制

SpringSecurity大体上是由一堆Filter实现的,Filter会在SpringMVC前拦截请求。Filter包括登出Filter(LogoutFilter)、用户名密码验证Filter(UsernamePasswordAuthenticationFilter)之类。Filter在交由其他组件完成细分的功能,最常用的UsernamePasswordAuthenticationFilter会持有一个AuthenticationManager引用,AuthenticationManager是一个验证管理器,专门负责验证。但AuthenticationManager本身并不做具体的验证工作,AuthenticationManager持有一个AuthenticationProvider集合,AuthenticationProvider才是做验证工作的组件,验证成功或失败之后调用对应的Handler。

简单SpringBootSecurity应用

接下来通过一个简单的SpringBoot案例,来了解一下SpringSecurity的基本使用。创建一个SpringBoot项目,添加SpringSecurity和Thymeleaf依赖。

案例代码下载地址:https://github.com/machaoyin/security-demo

完整的案例项目结构。

pom.xml完整代码。

<?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.2.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.mcy</groupId>
    <artifactId>security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-demo</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-web</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-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </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>
    </dependencies>

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

</project>

用于测试的HTML页面,用了bootstrap的样式文件。

login.html登录页面,主要有一个form表单和账号密码输入框,用于向login请求提交username和password,从而进行登录。Security默认登录地址login,退出地址为logout。代码如下。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Security登录</title>
    <link rel="stylesheet" href="static/bootstrap/dist/css/bootstrap.css">
</head>
<body>
    <div class="panel panel-primary" style="width: 500px; margin: 5% auto;">
        <div class="panel-heading">登录</div>
        <div class="panel-body">
            <form action="login" method="post" class="form-horizontal">
                <!--用户名或密码错误提示-->
                <div th:if="${param.error != null}">
                    <div class="alert alert-danger">
                        <p color="red">用户名或密码错误!</p>
                    </div>
                </div>
                <!--注销提示-->
                <div th:if="${param.logout != null}">
                    <div class="alert alert-success">
                        <p color="red">注销成功!</p>
                    </div>
                </div>

                //sec:authorize可以判断用户是否登录,用户权限,设置该菜单是否显示
                <p sec:authorize="isAnonymous()">未登录显示</p>
                <p sec:authorize="isAuthenticated()">登录显示</p>

                <div class="input-group input-sm">
                    <label class="input-group-addon"><i class="glyphicon glyphicon-user"></i></label>
                    <input type="text" class="form-control" name="username" placeholder="请输入用户名">
                </div>
                <div class="input-group input-sm">
                    <label class="input-group-addon"><i class="glyphicon glyphicon-lock"></i></label>
                    <input type="password" class="form-control" name="password" placeholder="请输入密码">
                </div>
                <div class="form-actions">
                    <input type="submit" class="btn btn-block btn-primary btn-default" value="登录">
                </div>
            </form>
        </div>
    </div>
</body>
</html>

用于用户名或密码错误提示。

<div th:if="${param.error != null}">
    <div class="alert alert-danger">
        <p color="red">用户名或密码错误!</p>
    </div>
</div>

退出,注销提示。

<div th:if="${param.logout != null}">
    <div class="alert alert-success">
        <p color="red">注销成功!</p>
    </div>
</div>

注意这些提示都是SpringSecurity里边自带的。

sec:authorize用于判断用户是否登录,用户是否拥有哪些角色权限,一般在前台页面控制菜单是否显示。

home.html是ROLE_USER用户登录之后显示的页面,同时提供了一个超链接到admin页面,代码如下。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Home页面</title>
    <link rel="stylesheet" href="static/bootstrap/dist/css/bootstrap.css">
</head>
<body>
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Home页面</h3>
        </div>
    </div>
    <h3>
        <p>欢迎[<span color="red" th:text="${user}">用户名</span>]访问Home页面!
        您的权限是:<span color="red" th:text="${role}">权限</span></p>
        <p><a href="admin">访问admin页面</a></p>
        <p><a href="logout">安全退出</a></p>
    </h3>
</body>
</html>

admin.html是ROLE_ADMIN用户登录之后显示的页面,同时提供了一个到dba页面的超链接,代码如下。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Admin页面</title>
    <link rel="stylesheet" href="static/bootstrap/dist/css/bootstrap.css">
</head>
<body>
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">Admin页面</h3>
        </div>
    </div>
    <h3>
        <p>欢迎[<span color="red" th:text="${user}">用户名</span>]访问Admin页面!
            您的权限是:<span color="red" th:text="${role}">权限</span></p>
        <p><a href="dba">访问dba页面</a></p>
        <p><a href="logout">安全退出</a></p>
    </h3>
</body>
</html>

dba.html页面只是显示简单的欢迎语句,代码如下。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>dba页面</title>
    <link rel="stylesheet" href="static/bootstrap/dist/css/bootstrap.css">
</head>
<body>
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">dba页面</h3>
        </div>
    </div>
    <h3>
        <p>欢迎[<span color="red" th:text="${user}">用户名</span>]访问dba页面!
            您的权限是:<span color="red" th:text="${role}">权限</span></p>
        <p><a href="logout">安全退出</a></p>

        
        <p sec:authorize="isAnonymous()">未登录显示</p>
        <p sec:authorize="isAuthenticated()">登录显示</p>
        <p sec:authorize="hasRole('ROLE_ADMIN')">权限包含ROLE_ADMIN显示</p>
        <p sec:authorize="!hasRole('ROLE_ADMIN')">权限不包含ROLE_ADMIN登录显示</p>
    </h3>
</body>
</html>

accessDenied.html是访问拒绝页面,如果登录的用户没有权限访问该页面,会进行提示,代码如下。

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>访问拒绝页面</title>
    <link rel="stylesheet" href="static/bootstrap/dist/css/bootstrap.css">
</head>
<body>
    <div class="panel panel-primary">
        <div class="panel-heading">
            <h3 class="panel-title">AccessDenied页面</h3>
        </div>
    </div>
    <h3>
        <p>欢迎[<span color="red" th:text="${user}">用户名</span>],您没有权限访问页面!
            您的权限是:<span color="red" th:text="${role}">权限</span></p>
        <p><a href="logout">安全退出</a></p>
    </h3>
</body>
</html>

在项目中新建一个security包,在该包下新建一个WebSecurityConfig类,继承WebSecurityConfigurerAdapter类,用于处理SpringSecurity的用户认证和授权操作,设置页面访问权限。

configure(AuthenticationManagerBuilder auth)和configure(HttpSecurity http)两个方法中分别打印了一句话,用于启动项目时的跟踪调试。

successHandler(new LoginSuccessHandle())用于处理登出成功之后的操作,LoginSuccessHandle类用于处理不同用户跳转到不同页面。

具体代码如下。

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;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 用户认证操作
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println("WebSecurityConfig   configure(AuthenticationManagerBuilder auth) 方法被调用。。。。。。");
        //添加用户,并给予权限
        auth.inMemoryAuthentication().withUser("aaa").password("{noop}1234").roles("USER");
        auth.inMemoryAuthentication().withUser("admin").password("{noop}admin").roles("ADMIN", "DBA");
    }

    /**
     * 用户授权操作
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("WebSecurityConfig   configure(HttpSecurity http) 方法被调用。。。。。。");
        http.csrf().disable();    //安全器令牌
        http.authorizeRequests()    //开始请求权限配置。
                .antMatchers("/login", "/static/**").permitAll()
                .antMatchers("/", "/home").hasRole("USER")
                .antMatchers("/admin/**").hasAnyRole("ADMIN", "DBA")
                .anyRequest().authenticated()   //其余所有的请求都需要认证(用户登录)之后才可以访问。
                .and()
                .formLogin()    //开始设置登录操作
                .loginPage("/login")   //设置登录页面的访问地址
                //.defaultSuccessUrl("/main")  //指定登录成功后转向的页面。
                .successHandler(new LoginSuccessHandle())    //登录成功跳转,LoginSuccessHandle处理不同权限跳转不同页面
                .failureUrl("/login?error")    //指定登录失败后转向的页面和传递的参数。
                .and()
                .logout().permitAll()       //退出
                .and()
                .exceptionHandling().accessDeniedPage("/accessDenied"); //指定异常处理页面
    }
}

认证成功处理类LoginSuccessHandle,实现了AuthenticationSuccessHandler接口,是Spring用来处理用户认证授权并跳转到指定URL的。

重新onAuthenticationSuccess方法,获取当前用户的权限,根据权限跳转到指定的URL路径,代码如下。

import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.IOException;
import java.util.Set;

public class LoginSuccessHandle  implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)throws IOException {
        //authentication.getAuthorities() 获取当前用户的权限
        Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());

        //获取到登陆者的权限,然后做跳转
        if (roles.contains("ROLE_ADMIN")){
            response.sendRedirect("/admin");
            return;
        }else if (roles.contains("ROLE_USER")){
            response.sendRedirect("/home");
            return;
        }else {
            response.sendRedirect("/accessDenied");
        }
    }
}

新建一个IndexController控制器,提供响应login,home,admin,dba,AccessDenied请求的方法。每个方法通过getUsername()方法获得当前认证用户的用户名,通过getAuthorith()方法获取当前认证用户的权限,并设置到ModelMap当中。

import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.ArrayList;
import java.util.List;

@Controller
public class IndexController {
    @RequestMapping(value = {"/", "/login"}, method = RequestMethod.GET)
    public String index(){
        return "login";
    }

    @RequestMapping("/home")
    public String homePage(ModelMap map){
        map.put("user", getUsername());
        map.put("role", getAuthority());
        return "home";
    }

    @RequestMapping("/admin")
    public String admin(ModelMap map){
        map.put("user", getUsername());
        map.put("role", getAuthority());
        return "admin";
    }

    @RequestMapping("/dba")
    public String dba(ModelMap map){
        map.put("user", getUsername());
        map.put("role", getAuthority());
        return "dba";
    }

    @RequestMapping("/accessDenied")
    public String accessDenied(ModelMap map){
        map.put("user", getUsername());
        map.put("role", getAuthority());
        return "accessDenied";
    }

    /**
     * 获取当前用户名称
     * @return
     */
    private String getUsername(){
        //获取当前用户名称
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        System.out.println("username="+username);
        return username;
    }

    private String getAuthority(){
        //获得Authentication对象,表示用户认证信息
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        List<String> roles = new ArrayList<>();
        //将角色名称添加到List集合
        for(GrantedAuthority a: authentication.getAuthorities()){
            roles.add(a.getAuthority());
        }
        System.out.println("role="+roles);
        return roles.toString();
    }
}

测试应用

运行项目可以看到WebSecurityConfig类中重写的两个方法已经执行,说明自定义的用户认证和用户授权工作已经生效。

在浏览器中输入URL测试应用:

http://localhost:8080 “/”、“/login”、“/home”、“admin”等任何一个当前项目的请求都会被重定向到http://localhost:8080/login页面,因为没有登录,,用户没有访问权限。login页面中,用sec:authorize写了两个标签一个是登录成功显示,一个是未登录显示,没有登录,就只显示了一个。sec:authorize一般用于控制菜单是否显示,效果如图。

输入错误的账号密码,会有提示用户名或密码错误。如图。

输入用户名:aaa,密码:1234,登录,该用户是“ROLE_USER”,跳转到home页面,如图。

单击超链接“访问admin页面”,由于当前用户的权限只是“ROLE_USER”,不能访问admin页面,所以会跳转到访问拒绝页面,如图。

单击超链接“安全退出”,会退出到登录页面,登录页面提示“注销成功!”,如图。

输入用户名:admin,密码admin,登录,该用户是“ROLE_USER”和“ROLE_DBA”权限,跳转到admin页面,如图。

单击超链接“访问dba页面”,由于当前用户的权限是“ROLE_ADMIN”和“ROLE_DBA”,可以访问dba页面,所以会跳转到dba页面,如图。在dba页面代码中,写了四行用sec:authorize判断是否显示的标签,两行判断是否登录,两行判断该用户是否有这个角色,条件成立才显示标签内容,效果如图。

通过测试可以看到,项目已经使用SpringSecurity实现了用户认证和用户授权。

上面的案例并没有把用户和权限存放到数据中,在实际开发中,用户和权限肯定是存放在数据库中,下面就来看看如何把数据存放到数据库中。

SpringBoot整合Springsecurity将用户和权限保存在数据库中

修改后项目的目录结构。

在原有的项目中,修改pom.xml文件,添加MySQL和JPA依赖。

这里使用的是JPA操作数据库,对JPA不了解的可以访问Spring-Data-Jpa入门

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

配置基本属性。

在src/main/resources下找到application.properties文件,在该配置文件中配置数据源和jpa相关的属性,需要在数据库中先新建一个security数据库。

#数据源信息配置
#数据库地址,jpa数据库名,需要在数据库中先建一个jpa数据库
spring.datasource.url=jdbc:mysql://localhost:3306/security?serverTimezone=GMT%2B8
#用户名
spring.datasource.username=root
#密码
spring.datasource.password=root
#链接数据库驱动
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#指定数据库方言
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#配置在日志中显示SQL语句
spring.jpa.show-sql=true
#指定自动创建|更新|验证数据库表结构等配置,配置成updata
#表示如果数据库中存在持久化类对应的表就不创建,不存在就创建对应的表
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.use-new-id-generator-mappings=false

创建用户类和角色权限类

SysUser权限类,代码如下。

import javax.persistence.*;

@Entity
@Table(name = "sys_role")
public class SysRole {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;     //主键
    private String name;   //权限名称

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

SysUser用户类

import javax.persistence.*;
import java.util.List;

@Entity
@Table(name = "sys_user")
public class SysUser {
    private Integer id;
    private String name;
    private String username;
    private String password;
    private List<SysRole> roles;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @ManyToMany(cascade=CascadeType.REFRESH,fetch=FetchType.EAGER)
    @JoinTable(name="sys_user_role",joinColumns=@JoinColumn(name="user_id"),inverseJoinColumns=@JoinColumn(name="role_id"))
    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }
}

SysUser类用来保存用户数据,其中username表示用户名,password表示密码,name表示用户姓名,roles表示用户权限的List集合,用户和权限的关系是多对多关系。

创建数据访问接口

在项目中新建一个repository包,在该包下新建一个UserRepository接口和RoleRepository接口,继承JpaRepository接口,UserRepository接口中写一个根据用户名去查询的方法findByUsername,遵循Spring-Data-Jpa命名规范,代码如下。

import com.mcy.securitydemo.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<SysUser, Integer> {
    //根据登录用户名查询用户
    SysUser findByUsername(String username);
}
import com.mcy.securitydemo.entity.SysRole;
import org.springframework.data.jpa.repository.JpaRepository;

public interface RoleRepository extends JpaRepository<SysRole, Integer> {

}

创建自定义服务类Service

在项目中新建一个service包,在该包下新建一个UserService类和RoleService类,UserService类实现了UserDetailsService接口,登录认证的时候SpringSecurity会通过UserDetailsService的loadUserByUsername()方法获取对应的UserDetails进行认证。

UserService类重写了UserDetailsService接口中的loadUserByUsername()方法,在方法中调用持久层接口的findByUsername方法通过JPA进行数据库验证,传递的参数是页面接收到的username。最后将获得的用户名,密码和权限保存到org.springframework.security.core.userdetails.User类中并返回,该User类是SpringSecurity内部的实现,专门用于保存用户名,密码,权限等与认证相关的信息。

UserService类代码

import com.mcy.securitydemo.entity.SysRole;
import com.mcy.securitydemo.entity.SysUser;
import com.mcy.securitydemo.repository.UserRepository;
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.User;
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;
import java.util.ArrayList;
import java.util.List;

/**
 * 需要实现UserDetailsService接口
 * 因为在SpringSecurity中配置的相关参数需要是UserDetailsService类的数据
 */
@Service
public class UserService implements UserDetailsService {

    //注入持久层接口UserRepository
    @Autowired
    private UserRepository userRepository;

    /**
     * 重写UserDetailsService接口中的loadUserByUsername方法,通过该方法查询对应的用户
     * 返回对象UserDetails是SpringSecurity的一个核心接口。
     * 其中定义了一些可以获取用户名,密码,权限等与认证相关信息的方法。
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //调用持久层接口findByUsername方法查询用户。
        SysUser user = userRepository.findByUsername(username);
        if(user == null){
            throw new UsernameNotFoundException("用户名不存在");
        }
        //创建List集合,用来保存用户权限,GrantedAuthority对象代表赋予当前用户的权限
        List<GrantedAuthority> authorities = new ArrayList<>();
        //获得当前用户权限集合
        List<SysRole> roles = user.getRoles();
        for(SysRole role: roles){
            //将关联对象role的name属性保存为用户的认证权限
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        //此处返回的是org.springframework.security.core.userdetails.User类,该类是SpringSecurity内部的实现
        //org.springframework.security.core.userdetails.User类实现了UserDetails接口
        return new User(user.getUsername(), user.getPassword(), authorities);
    }

    //保存方法
    public void save(SysUser user) {
        userRepository.save(user);
    }
}

RoleService类代码

import com.mcy.securitydemo.entity.SysRole;
import com.mcy.securitydemo.repository.RoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class RoleService {
    @Autowired
    private RoleRepository roleRepository;

    public void save(SysRole role) {
        roleRepository.save(role);
    }
}

修改WebSecurityConfig类设置认证方式,修改后的WebSecurityConfig代码如下。

import com.mcy.securitydemo.service.UserService;
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.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    /**
     * 用户认证操作
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        System.out.println("WebSecurityConfig   configure(AuthenticationManagerBuilder auth) 方法被调用。。。。。。");
        //添加用户,并给予权限
        auth.inMemoryAuthentication().withUser("aaa").password("{noop}1234").roles("USER");
        auth.inMemoryAuthentication().withUser("admin").password("{noop}admin").roles("ADMIN", "DBA");
        //设置认证方式
        auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
    }

    /**
     * 用户授权操作
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        System.out.println("WebSecurityConfig   configure(HttpSecurity http) 方法被调用。。。。。。");
        http.csrf().disable();    //安全器令牌
        http.authorizeRequests()    //开始请求权限配置。
                .antMatchers("/login", "/static/**").permitAll()
                .antMatchers("/", "/home").hasRole("USER")
                .antMatchers("/admin/**").hasAnyRole("ADMIN", "DBA")
                .anyRequest().authenticated()   //其余所有的请求都需要认证(用户登录)之后才可以访问。
                .and()
                .formLogin()    //开始设置登录操作
                .loginPage("/login")   //设置登录页面的访问地址
                //.defaultSuccessUrl("/main")  //指定登录成功后转向的页面。
                .successHandler(new LoginSuccessHandle())    //登录成功跳转,LoginSuccessHandle处理不同权限跳转不同页面
                .failureUrl("/login?error")    //指定登录失败后转向的页面和传递的参数。
                .and()
                .logout().permitAll()       //退出
                .and()
                .exceptionHandling().accessDeniedPage("/accessDenied"); //指定异常处理页面
    }
}

测试应用

打开MySQL数据库,新建security数据库。运行项目根据对象之间关系,JPA会在数据库中自动创建sys_user, sys_role和中间表sys_user_role三张表。

test类位置

因为数据库中没有数据,可以通过test类添加测试数据,添加了三个权限和两个用户。在test类中运行如下代码。

@Autowired
private RoleService roleService;
@Autowired
private UserService userService;

@Test
void contextLoads() {
SysRole role = new SysRole();
role.setName("ROLE_USER");
roleService.save(role);
SysRole role1 = new SysRole();
role1.setName("ROLE_ADMIN");
roleService.save(role1);
SysRole role2 = new SysRole();
role2.setName("ROLE_DBA");
roleService.save(role2);

SysUser user = new SysUser();
user.setName("张三");
//用于对密码加密
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
user.setPassword(encoder.encode("123456"));
user.setUsername("123456");
List<SysRole> roles = new ArrayList<>();
roles.add(role);    //给123456用户添加ROLE_USER权限
user.setRoles(roles);
userService.save(user);

SysUser user1 = new SysUser();
user1.setName("李四");
//用于对密码加密
user1.setPassword(encoder.encode("123456"));
user1.setUsername("system");
List<SysRole> roles1 = new ArrayList<>();
roles1.add(role1);    //给system用户添加ROLE_USER权限
roles1.add(role2);
user1.setRoles(roles1);
userService.save(user1);

运行后数据库中数据如图(密码是加密过后的密码)

在浏览器中输入http://localhost:8080/login访问登录页面,登录用户名为123456, 密码为123456的用户。登录成功页面如图。

和上面aaa用户权限一样,所以能访问的页面也和aaa用户一样。system用户所添加的权限和上面的admin用户权限一样,大家还可以自行添加权限和用户进行测试。

最后在附上修改后的下载地址https://github.com/machaoyin/security-demo

有什么问题欢迎下方留言,如果对你有帮助,帮忙顺手点赞关注一下。

发布了112 篇原创文章 · 获赞 223 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/qq_40205116/article/details/103439326