Spring Security 是基于 Spring 的一款安全框架,主要包含认证和授权两大模块,在这一点上与其他的安全框架大同小异,如 Apache Shiro。Shiro 想必大家都不陌生,无论是在实现还是应用上相比较而言,都可以说比 Spring Security 要简单。但是我想目前做 Web 开发的后端的框架基本上选择的都是 Spring,那么就功能拓展系统兼容性方面来讲,无疑是同为一家的 Spring Security 更有发言权。

下面我们简单了解一下 Spring Security

🚀 源码地址:https://github.com/NekoChips/SpringDemo/10.springboot-security

🚀 官方文档:Spring Security 官方文档

这里我使用 SpringBootSpringSecurity 进行集成,SpringBoot 版本为 SpringBoot 2.1.9.RELEASE

1. 环境搭建

创建一个 Maven 项目后,引入 SpringSecurity 依赖,由于 SpringBoot 中包含了 SpringSecurityStarter,我们可以直接拿来使用。

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

创建启动类,然后创建一个静态页面或者创建一个 Controller 当做项目的访问资源,启动项目,会在控制台中发现一串信息

Using generated security password: 7402060e-d925-4c5a-85b2-7a6a40d04d28

访问服务资源,页面会跳转至一个登录页面,这个页面就是 SpringSecurity 提供的。

image.png

这里的信息输入什么呢?SpringSecurity 提供的默认用户名为 user,密码即为之前控制台输出的那一串 UUID

同样的我们可以在使用在配置文件中配置的用户名和密码来进行验证。

spring:
  security:
    user:
      name: admin
      password: 123456

2. 启用 Spring Security

Spring Security 其实是借用一系列的 Filter 来提供各种安全性功能。DelegationFilterProxy 是一个特殊的 Servlet Filter,它本身所做的工作不多,它的主要职责是将工作委托给一个 Filter 的实现类。而查看源码会发现,在 Spring Security 的配置类中声明了一个名为 springSecurityFilterChainFilter ,而前面说到的 DelegationFilterProxy 会将具体的过滤逻辑委托给它进行处理。

@Configuration
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
	......

	@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
	public Filter springSecurityFilterChain() throws Exception {
		boolean hasConfigurers = webSecurityConfigurers != null
				&& !webSecurityConfigurers.isEmpty();
		if (!hasConfigurers) {
			WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
					.postProcess(new WebSecurityConfigurerAdapter() {
					});
			webSecurity.apply(adapter);
		}
		return webSecurity.build();
	}

	......
}

其中 DEFAULT_FILTER_NAME 的值为 springSecurityFilterChain

当我们启用 Spring Security 的时候,会自动创建一些 Filter 来实现安全配置。启用 Spring Security 非常简单,这里我们使用 Java Config 的方式开启 Spring Security。


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
}

添加 @EnableWebSecurity 即会启用 Web 的安全功能,但其本身只起到声明作用,为了配置 Spring Security 我们需要实现 WebSecurityConfigurer,而 Spring 提供的 WebSecurityConfigurerAdapter 可以很好地帮我们完成这部分工作。

3. 自定义表单认证

上面使用的认证方式是 SpringSecurity 自带的表单认证,我们可以将其为自定义的表单认证方式。

👀 SpringBoot 1.x 的版本默认支持的是 HTTP Basic 认证方式, 从 SpringBoot 2.x 版本开始,它默认支持的是表单认证的方式。HTTP Basic 认证方式更多地应用在接口的访问上,而表单认证方式更多地就应用在页面访问上了。

我们可以通过重写 WebSecurityConfigurerAdapter 的一个或多个方法来实现 Web 中安全性的更多细节,而不是使用 Spring Security 提供的默认功能。

我们可以通过重写 WebSecurityConfigurerAdapter 的三个 configure() 方法来配置 Spring Security 的安全性:

  • config(WebSecurity) : 通过重写,配置 Spring Security 的 Filter Chain。
  • configure(HttpSecurity) : 通过重写,配置如何通过拦截器保护请求。
  • configure(AuthenticationManagerBuilder) :通过重写,配置 userDetails 服务

而本节中需要实现的自定义表单认证功能,就要通过 重写 configure(HttpSecurity) 来实现了:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 开启表单认证方式
        http.formLogin()
                // 配置登录页面
                .loginPage("/login.html")
                // 处理表单登录,login为Security自带的登录处理,直接用就可以了
                .loginProcessingUrl("/login")

                // 配置相关授权
                .authorizeRequests()
                // permitAll() 表示不拦截访问 login.html 的请求
                .antMatchers("/login.html").permitAll()
                // 其他所有的请求都需要经过认证
                .anyRequest().authenticated()

                .and()
		// 关闭跨域请求伪造
                .csrf().disable();
    }
}
  • formLogin() 方法启用了基本的登录页功能,就是第一节中的登录界面截图。
  • loginPage() 方法指定自定义的登录页面。
  • loginProcessingUrl() 方法指定处理登录的接口,Spring Security 提供了 /login 接口可以直接调用。

上述这样就指定了我们自定义的认证表单,静态文件放在项目 src/resources/static 目录下即可。

3.1. 配置转发路径

SpringSecurity 支持配置登录成功和失败跳转后的转发路径

先写两个测试接口

@RestController
@RequestMapping
public class TestController {

    @PostMapping("hello")
    public String hello() {
        return "Hello Security";
    }

    @PostMapping("fail")
    public String loginFail() {
        return "login fail";
    }
}

SecurityConfig 中进行配置

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启表单认证方式
        http.formLogin()
                // 配置登录页面
                .loginPage("/login.html")
                // 处理表单登录,login为Security自带的登录处理,直接通就可以了
                .loginProcessingUrl("/login")
		// 配置登录成功转发的请求路径
                .successForwardUrl("/hello")
		// 配置登录失败转发的请求路径
                .failureForwardUrl("/fail")

                .and()
                // 配置相关授权
                .authorizeRequests()
                // permitAll() 表示不拦截访问 login.html 的请求
                .antMatchers("/login.html","/fail").permitAll()
                // 其他所有的请求都需要经过认证
                .anyRequest().authenticated()

                .and()
                // 关闭跨域请求伪造
                .csrf().disable();
    }
}

启动项目后,首先输入正确的密码,注意地址栏,页面转发到了设定的 successForwardUrl 指定的请求路径

image.png

下面我们输入错误的密码,页面转发到了设定的 failureForwardUrl 指定的请求路径

image.png

3.2. 配置处理器

除了配置转发路径的方式对登录后的逻辑进行处理外,我们还可以通过配置 successHandlerfailureHandler 分别执行登录成功和失败后的逻辑。

编写一个自定义认证失败处理器 MyAuthenticationFailureHandler 实现 AuthenticationFailureHandler 接口,并实现 onAuthenticationFailure(...) 方法。

👀 这里我配置为登录失败后返回登录页面

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        response.sendRedirect("/login.html");
    }
}

编写一个自定义认证成功处理器 MyAuthenticationSuccessHandler 实现 AuthenticationSuccessHandler 接口,并实现 onAuthenticationSuccess(...) 方法。

👀 这里我设置为登录成功后,重定向请求 /authInfo 接口

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {

        // 配置登录成功后的页面重定向请求路径
        redirectStrategy.sendRedirect(request, response, "/authInfo");
    }
}

TestController 中添加请求接口

    @GetMapping("authInfo")
    public Authentication authInfo(Authentication authentication) {
        return authentication;
    }

配置 SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启表单认证方式
        http.formLogin()
                // 配置登录页面
                .loginPage("/login.html")
                // 处理表单登录,login为Security自带的登录处理,直接通就可以了
                .loginProcessingUrl("/login")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
//                .successForwardUrl("/hello")
//                .failureForwardUrl("/fail")

                .and()
                // 配置相关授权
                .authorizeRequests()
                // permitAll() 表示不拦截访问 login.html 的请求
                .antMatchers("/login.html","/fail").permitAll()
                // 其他所有的请求都需要经过认证
                .anyRequest().authenticated()

                .and()
                // 关闭跨域请求伪造
                .csrf().disable();
    }
}

重启项目,尝试登录成功和登录失败,查看页面响应。这里就不演示了。

3.3. RememberMe 功能实现

在网页的登录界面中一般都会有一个 “记住我” 的功能选择框,勾选后登录成功的用户在之后的一段时间内,用户无需进行登录验证即可对系统资源进行访问。通常实现这个功能都是使用浏览器的 cookie,而 cookie 中存在服务器颁发给客户端的访问 token,从而使用户无需登录即可访问系统资源。

👀 SpringSecurity 也是使用上述的方式来实现 Remember Me 功能,好在它帮助开发者进行了封装,无需开发者自己去实现。

SpringSecurity 实现该功能的大致过程如下图:

graph TB
    勾选RememberMe-->用户登录--> 生成包含用户登录信息的token
    用户请求资源-->浏览器没有对应cookie--> 生成包含用户登录信息的token
    浏览器有对应cookie-->cookie过期-->生成包含用户登录信息的token
    用户请求资源-->浏览器有对应cookie-->cookie未过期-->服务器根据cookie获取对应token
    生成包含用户登录信息的token-->token持久化并生成包含该token的cookie-->将cookie返回给浏览器-->服务器根据cookie获取对应token
    服务器根据cookie获取对应token-->帮助用户自动完成登陆操作-->访问系统资源

从上图中我们可以看出,实现该功能需要将 token 持久化。查看 SpringSecurityrememberme 中的源码,我们可以发现 SpringSecurity 是通过 PersistentTokenRepository 接口对 token 进行持久化管理。

PersistentTokenRepository 拥有两个子类, InMemoryTokenRepositoryImplJdbcTokenRepositoryImpl

这里我们使用 JdbcTokenRepositoryImpl 对 token 进行持久化管理。

首先我们需要添加数据库的相关依赖

        <!-- 数据库连接依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

application.yml 文件中添加数据库配置

spring:
  ## 数据库
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/demo?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: demo
    password: demo

查看 JdbcTokenRepositoryImpl 源码

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements
		PersistentTokenRepository {

	public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, "
			+ "token varchar(64) not null, last_used timestamp not null)";
	public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
	public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
	public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
	public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";

	private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL;
	private String insertTokenSql = DEF_INSERT_TOKEN_SQL;
	private String updateTokenSql = DEF_UPDATE_TOKEN_SQL;
	private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL;
	private boolean createTableOnStartup;
......
}

源码中建表语句已经给出,使用建表语句创建表。

准备就绪后,在 SecurityConfig 类中配置 token 持久化

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	......

    // 数据源
    @Autowired
    private DataSource dataSource;

    // jdbcToken 持久化对象
    @Bean
    public PersistentTokenRepository tokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(false);
        return jdbcTokenRepository;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 开启表单认证方式
        http.formLogin()
                // 配置登录页面
                .loginPage("/login.html")
                // 处理表单登录,login为Security自带的登录处理,直接通就可以了
                .loginProcessingUrl("/login")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
//                .successForwardUrl("/hello")
//                .failureForwardUrl("/fail")

                .and()
                // 配置相关授权
                .authorizeRequests()
                // permitAll() 表示不拦截访问 login.html 的请求
                .antMatchers("/login.html","/fail").permitAll()
                // 其他所有的请求都需要经过认证
                .anyRequest().authenticated()

                .and()
                // rememberMe 相关配置
                .rememberMe()
                // token持久化仓库
                .tokenRepository(tokenRepository())
                // 过期时间,1个小时
                .tokenValiditySeconds(3600)

                .and()
                // 关闭跨域请求伪造
                .csrf().disable();
    }

	......
}

👀 这里我们将用户信息放在内存中进行验证,开发环境中是要将用户信息存入数据库中。

重启项目,勾选 记住我 登录成功后,发现数据表中多出一条数据,这说明登录成功后 SpringSecurity 成功的对 token 进行了持久化,并且也为浏览器生成了对应的 cookie。在 cookie 过期之前,用户依然可以无需登录验证即可访问系统资源,这里我就不做演示了。

image.png

👀 SpringBoot 2.x 之后的版本,在配置了 rememberme 功能时,它自带的表单登录页面上也会显示出该复选框,这对初学者来说还是非常友好的。

4. Session 管理

前面我们使用的浏览器 cookie 包含 token 的方式可称之为 无状态,意为服务器端没有存储用户信息,这也是目前最主流的处理 免密登录 的方式。

然而,出于某些原因需要在服务器端存储用户信息,这时候就需要用到 session,使用 session 时由于服务端需要根据 session 信息来判断用户的状态,所以我们称之为 有状态 的。

这里就简单的介绍下 session,这里不做深入研究。

4.1. 配置 session 管理

SpringSecurity 中配置 session 管理非常简单,只需要在 SecurityConfig 中做相关配置即可。

👀 这里避免造成混淆,暂时先关闭 remember me 功能。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	......

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单认证方式
        http.formLogin()
                // 配置登录页面
                .loginPage("/login.html")
                // 处理表单登录
                .loginProcessingUrl("/login")
                // 配置认证成功处理器
                .successHandler(successHandler)
                // 配置认证失败处理器
                .failureHandler(failureHandler)
                .and()

                // 配置相关授权
                .authorizeRequests()
                // 不拦截的请求
                .antMatchers("/login.html","/session/invalid", "/fail").permitAll()
                // 所有的请求都需要经过认证
                .anyRequest().authenticated()

                .and()
                // 添加session管理器
                .sessionManagement()
                // session 失效后跳转的地址
                .invalidSessionUrl("/session/invalid")

                .and()
                // 关闭跨域请求伪造
                .csrf().disable();
    }

	......
}

TestController 新增 /session/invalid 接口

    @GetMapping("session/invalid")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String sessionExpired() {
        return "session expired";
    }

application.yml 文件中配置 session 过期时间

server:
  address: localhost
  port: 8080
  servlet:
    session:
      timeout: 60

👀 这里是 SpringBoot 2.x 之后版本的设置方式,查看源码可以发现,改配置时间的单位是秒,默认时长为 30 分钟。

public class Session {

	@DurationUnit(ChronoUnit.SECONDS)
	private Duration timeout = Duration.ofMinutes(30);

	private Set<Session.SessionTrackingMode> trackingModes;

	private boolean persistent;

	......
}

👀 SpringBoot 1.x 版本设置方式有所不同

spring:
  session:
    timeout: 60

配置好后重启项目,完成登录一分钟后再刷新页面

image.png

4.2. 限制用户登录个数

在使用应用时,经常会碰见账号在一处登陆后,就不能再别的地方登陆或者账号被挤掉。

SpringSecurity 中也可以非常方便地对其进行配置,在 SecurityConfig 配置 session 的位置新增一项 maximumSessions(int maximumSessions) 配置即可。

	......

               .and()
                // 添加session管理器
                .sessionManagement()
                // session 失效后跳转的地址
                .invalidSessionUrl("/session/invalid")
                .maximumSessions(1)
		.and()

	.......

👀 配置 maximumSessions(int maximumSessions) 后需要在后面新增一个 .and() 将执行之后的对象转换为 SessionManagementConfigurer(HttpSecurity) 对象。

重启项目,首先在 Chrome 浏览器上登录访问系统资源,然后在另一个浏览器登录访问资源,返回 Chrome 浏览器刷新页面,会出现被挤掉的系统提示。

image.png

同样我们也可以自定义系统提示。

创建 MySessionExpiredStrategy 实现 SessionInformationExpiredStrategy 接口

@Component
public class MySessionExpiredStrategy implements SessionInformationExpiredStrategy {
    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        HttpServletResponse response = event.getResponse();
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write("您的账号已经在别的地方登录! 如果密码遭到泄露,请立即修改密码!");
    }
}

在上面的 SecurityConfig 配置的基础上新增 MySessionExpiredStrategy

	......

    // session 限制登录相关配置
    @Autowired
    private MySessionExpiredStrategy sessionExpiredStrategy;

        @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 表单认证方式
        http.formLogin()
		......

                .and()
                // 添加session管理器
                .sessionManagement()
                // session 失效后跳转的地址
                .invalidSessionUrl("/auth/session/invalid")
                .maximumSessions(1)
                .expiredSessionStrategy(sessionExpiredStrategy)
                .and()

                .and()
                // 关闭跨域保护
                .csrf().disable();
    }

	......

再次执行上面的操作,页面返回如下

image.png

👀 前面说到,一种操作是被挤掉,还有一种操作是登陆后不允许其他地方再次登录。SpringSecurity 同样可以对其进行配置。

在如上的 SecurityConfig 配置中新增 maxSessionsPreventsLogin(true)

	......

                .and()
                // 添加session管理器
                .sessionManagement()
                // session 失效后跳转的地址
                .invalidSessionUrl("/session/invalid")
                .maximumSessions(1)
                .expiredSessionStrategy(sessionExpiredStrategy)
                .maxSessionsPreventsLogin(true)
                .and()

	......

重启项目,再次重复上面的登录操作。发现 IE 浏览器中已经无法成功登录了。

4.3. 分布式 session 管理

现在由于 SpringCloud 强大的功能,分布式的应用也非常广泛,为了避免分布式应用之间需要用户进行重复认证,我们可以将用户 session 放进第三方容器中进行统一管理,来实现分布式 session 共享的功能。

首先引入 Redis 依赖以及 Redis 管理 session 依赖。

        <!-- redis 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <!-- 使用redis管理session依赖 -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

application.yml 文件中新增 session 储存策略为 Redis,并配置 Redis

spring:
  session:
    store-type: redis
  ## redis 配置
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    timeout: 3000

重启项目,登陆操作后查看 Redis

image.png

验证 分布式 session 的应用,复制该项目修改端口,只要在其中一个服务进行过登录,其他的服务上的系统资源均可以正常访问,可以自行验证。

4.4. 操作 session

SpringSecurity 提供的 SessionRegistry 支持对 ssession 进行操作。

查看源码

public interface SessionRegistry {
	// ~ Methods
	// ========================================================================================================

	List<Object> getAllPrincipals();

	List<SessionInformation> getAllSessions(Object principal,
			boolean includeExpiredSessions);

	SessionInformation getSessionInformation(String sessionId);

	void refreshLastRequest(String sessionId);

	void registerNewSession(String sessionId, Object principal);

	void removeSessionInformation(String sessionId);
}

获取所有 session 信息(包含已经失效的 Session):getAllPrincipals()

获取所有可用的 session 信息:getAllSessions(Object principal, boolean includeExpiredSessions)

获取 session 详情:getSessionInformation(String sessionId)

刷新 sessionrefreshLastRequest(String sessionId)

删除 sessionremoveSessionInformation(String sessionId)

5. 退出登录

SpringSecurity 对退出登录进行了一系列的封装处理,默认的退出路径为 /logout ,开发中直接使用是极为方便。它默认执行退出操作的大致流程如下:

graph TB
        用户退出登录-->使当前Session失效
        使当前Session失效-->清空RememberMe记录-->清空当前SecurityContext对象-->返回至登录页

我们也可以对上述流程进行配置

......

                .and()
                // 配置相关授权
                .authorizeRequests()
                // permitAll() 表示不拦截访问 login.html 的请求
                .antMatchers("/login.html","/fail","/session/invalid", "/logout/success").permitAll()
                // 其他所有的请求都需要经过认证
                .anyRequest().authenticated()

......

		// 退出配置
		.and()
    		.logout()
   		.logoutUrl("/logout")
    		.logoutSuccessUrl("/logout/success")
    		.deleteCookies("JSESSIONID")
		.and()

......

👀 退出成功后不返回默认的登录页面,跳转至指定的请求路径 /logout/success,删除名称为 JESSIONIDcookie

在 TestController 中新增 /logout/success 接口

    @GetMapping("logout/success")
    public String logoutSuccess() {
        return "Logout Success!";
    }

重启项目登录成功后,访问 /logout

image.png

6. 权限管理

权限管理一直是应用开发中非常重要的一项功能,常见的权限框架有 Apache Shiro ,SpringSecurity 同样也支持细粒度的权限控制,也提供了非常方便的注解方式进行权限控制,下面我们来了解一下。

6.1. UserDetails 和 GrantedAuthority

这两个接口是实现权限控制的两个主要的接口。

Shiro 一样,我们权限也使用 用户 <==> 角色 角色 <==> 权限 的方式实现对用户权限的控制。

👀 以下代码引入了 MybatisPlus 依赖,不想使用的无需添加该依赖,下面代码作相应修改即可。

User 需要实现 UserDetails

@TableName(value = "tb_user_info")
public class UserEntity implements Serializable, UserDetails {

    private static final long serialVersionUID = -517800012387095551L;

    @TableId(value = "U_ID", type = IdType.UUID)
    private String userId;

    @TableField(value = "U_NAME")
    private String username;

    @TableField(value = "U_PASSWORD")
    private String password;

    // -1: 过期; 0: 锁定; 1:启用
    @TableField(value = "U_STATUS")
    private Integer status;

    // 角色列表
    @TableField(exist = false)
    private Collection<RoleEntity> roles;

    // 权限列表
    @TableField(exist = false)
    private Collection<AuthorityEntity> authorities;

    public UserEntity() {

    }

    public UserEntity(String username, String password, Integer status) {
        this.username = username;
        this.password = password;
        this.status = status;
    }

	// 省略 getter、setter
	......

    // 获取用户的权限,这里可以配置基于方法的权限,也可以配置基于角色的权限
    // 同样可以把两个都配置进去
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        grantedAuthorities.addAll(this.authorities);
        grantedAuthorities.addAll(this.roles);
        return grantedAuthorities;
    }

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

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

    // 用户是否过期,过期返回false, 没过期返回true
    @Override
    public boolean isAccountNonExpired() {
        return !this.status.equals(CommonConstant.EXPIRED.getValue());
    }

    // 用户是否被锁住,锁定返回false,没锁定返回true
    @Override
    public boolean isAccountNonLocked() {
        return !this.status.equals(CommonConstant.LOCKED.getValue());
    }

    // 判断用户的凭证是否过期, 用于 refresh_token
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 判断用户是否可用
    @Override
    public boolean isEnabled() {
        return this.status.equals(CommonConstant.VALID.getValue());
    }

}

RoleAuthority 均需实现 GrantedAuthority

RoleEntity

@TableName(value = "tb_role")
public class RoleEntity implements GrantedAuthority {
    @TableId(value = "R_ID", type = IdType.AUTO)
    private Integer roleId;

    @TableField(value = "R_NAME")
    private String roleName;

    // 权限列表
    @TableField(exist = false)
    private Collection<AuthorityEntity> authorities;

	// 省略 getter、setter
	......

    @Override
    public String getAuthority() {
        return this.roleName;
    }
}

AuthorityEntity

@TableName(value = "tb_permission")
public class AuthorityEntity implements GrantedAuthority {

    @TableId(value = "PERM_ID", type = IdType.AUTO)
    private Integer authId;

    @TableField(value = "PERM_NAME")
    private String authName;

    @Override
    public String getAuthority() {
        return this.authName;
    }

	// 省略 getter、setter
	......
}

6.2. UserDetailsService 接口

UserDetailsService 接口是用户认证和授权的核心接口,用户登录时通过该接口验证用户信息合法性和授权操作。

编写 UserDetailsService 实现类

@Service
public class UserServiceImpl implements IUserService, UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private IRoleService roleService;

    @Autowired
    private IAuthService authService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * userDetailsService 方法实现
     * @param username 用户名
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = new UserEntity();
        user.setUsername(username);
        QueryWrapper<UserEntity> wrapper = new QueryWrapper<>(user);

        UserEntity existUser = userMapper.selectOne(wrapper);
        // 设置用户方法级权限
        existUser.setAuthorities(listAuthById(existUser.getUserId()));
        // 设置用户角色级权限
        existUser.setRoles(listRoleById(existUser.getUserId()));

        return new User(username, existUser.getPassword(), existUser.isEnabled(),
                existUser.isAccountNonExpired(), existUser.isCredentialsNonExpired(),
                existUser.isAccountNonLocked(), existUser.getAuthorities());
    }

	// 省略权限查询逻辑,自行实现
	......
}

👀 我们也可以直接在代码里代替数据库返回数据创建一个用户数据,并授予一个权限。修改实现的方法即可

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserEntity user = new UserEntity();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode("123456"));
        user.setStatus(CommonConstant.VALID.getValue());
        return new User(username, user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN","user:add"));
    }

上述代码我们授予了角色两个权限,一个 角色权限 admin,一个 接口权限 user:add

6.3. 使用注解

在 SecurityConfig 配置类上添加 @EnableGlobalMethodSecurity(prePostEnabled = true) 注解,开启用于权限认证的注解。

@Configuration
//@EnableWebSecurity
// 开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	......
}

在 TestController 中创建需要权限访问的接口

    @GetMapping("testAuth")
    @PreAuthorize("hasAuthority('user:add')")
    public String testAuth() {
        return "has authority to visit";
    }

    @GetMapping("testRole")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public String testRole() {
        return "has role to visit";
    }

    @GetMapping("authInfo")
    @PreAuthorize("hasAnyAuthority('auth:info','ROLE_SUPER')")
    public Authentication authInfo(Authentication authentication) {
        return authentication;
    }

👀 SpringSecurity 规定如果使用 hasRole 属性,角色权限必须以 ROLE_ 开头。

重启项目登录成功后,分别访问对应资源。

image.png

image.png

image.png

👀 403 即表示无权限访问。

7. 总结

上面所说只是 SpringSecurity 的一些基础的功能,还有一些其他较为强大的功能,还需要时间去研究。总之,SpringSecurity 是一款非常强大的安全权限框架,如果项目中涉及到权限认证这一块,SpringSecurity 绝对是最佳的不二选择。

🚀 源码地址:https://github.com/NekoChips/SpringDemo/10.springboot-security

🚀 相关链接:

  1. SpringSecurity OAuth2 集成使用:基于 SpringSecurity OAuth2 实现应用授权

关于作者:NekoChips
本文地址:https://chenyangjie.com.cn/articles/2020/03/15/1584254953580.html
版权声明:本篇所有文章仅用于学习和技术交流,本作品采用 BY-NC-SA 4.0 许可协议,如需转载请注明出处!
许可协议:知识共享许可协议