springsecurity简单入门

SpringSecurity简单入门

SpringSecurity 是一个灵活和强大的身份验证和访问控制的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成,并配备了流行的安全算法实现捆绑在一起。

环境:SpringBoot 2.1 + Mybatis + SpringSecurity 5.0

Spring Security 模块

  • 核心模块 - spring-security-core.jar:包含核心验证和访问控制类和接口,远程支持的基本配置API,是基本模块。
  • 远程调用 - spring-security-remoting.jar:提供与 Spring Remoting 集成。
  • 网页 - spring-security-web.jar:包括网站安全的模块,提供网站认证服务和基于URL访问控制。
  • 配置 - spring-security-config.jar:包含安全命令空间解析代码,若使用XML进行配置则需要。
  • LDAP - spring-security-ldap.jar:LDAP 验证和配置,若需要LDAP验证和管理LDAP用户实体。
  • ACL访问控制表 - spring-security-acl.jar:ACL专门领域对象的实现。
  • CAS - spring-security-cas.jar:CAS客户端继承,若想用CAS的SSO服务器网页验证。
  • OpenID - spring-security-openid.jar:OpenID网页验证支持。
  • Test - spring-security-test.jar:支持Spring Security的测试。

认证流程

  1. 用户使用用户名和密码登录。
  2. 用户名密码被过滤器(默认为UsernamePasswordAuthenticationFilter)获取到,封装成 Authentication。
  3. token(Authentication实现类)传递给 AuthenticationManager 进行认证。
  4. AuthenticationManager 认证成功后返回一个封装了用户权限信息的 Authentication 对象。
  5. 通过调用 SecurityContextHolder.getContext().setAuthentication(…) 将 Authentication 对象赋给当前的 SecurityContext。

启动原理

启动demo

引入相关依赖后,根据Spring Security官方文档给出的例子创建类。

1
2
3
4
5
6
7
8
9
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
}

以上我们便将spring security应用到我们的项目中了,上面的例子,在内存中配置了一个用户名为user,密码为password,并且拥有USER角色的用户。想要知道它是怎么运行的,请往下看。

WebSecurityConfigurer的子类可以扩展spring security的应用, 而WebSecurityConfigurerAdapter是WebSecurityConfigurer 的一个适配器,必然也是做了很多默认的工作。

入手 @EnableWebSecurity注解

跟进@EnableWebSecurity看下源码:

1
2
3
4
5
6
7
8
9
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
boolean debug() default false;
}

Tips:

@EnableWebSecurity 配置到拥有注解 @Configuration 的类上,就可以获取到spring security的支持.

跟进 SpringWebMvcImportSelector

SpringWebMvcImportSelector 的作用是判断当前的环境是否包含springmvc,因为spring security可以在非spring环境下使用,为了避免DispatcherServlet的重复配置,所以使用了这个注解来区分。

跟进 @EnableGlobalAuthentication

注解的源码如下:

1
2
3
4
@Import(AuthenticationConfiguration.class)
@Configuration
public @interface EnableGlobalAuthentication {
}

可以看出,这个注解引入了AuthenticationConfiguration配置。而这个类用来配置认证相关,主要任务就是生成全局的身份认证管理者。AuthenticationManager:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@Import(ObjectPostProcessorConfiguration.class)
public class AuthenticationConfiguration {
private AuthenticationManager authenticationManager;
@Bean
public AuthenticationManagerBuilder authenticationManagerBuilder(
ObjectPostProcessor<Object> objectPostProcessor) {
return new AuthenticationManagerBuilder(objectPostProcessor);
}

public AuthenticationManager getAuthenticationManager() throws Exception {
...
}
}

跟进 WebSecurityConfiguration

KZGylR.jpg

setFilterChainProxySecurityConfigurer()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
//T1 使用@Value获取到配置信息
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getebSecurityConfigurers()}") List<SecurityConfigurer<Filter,WebSecurity>> webSecurityConfigurers)
throws Exception {

//T2 创建一个webSecurity 对象
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}

//T3对configures进行排序
Collections.sort(webSecurityConfigurers,AnnotationAwareOrderComparator.INSTANCE);

//T4对Order进行比较是否有相同的,由于前面进行了排序,只要比较后有相同的就可以
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config :webSecurityConfigurers) {
Integer order =AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must beunique. Order of "
+ order + " was already used on " +previousConfig + ", so it cannot be usedon "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity>webSecurityConfigurer : webSecurityConfigurers) {
//T5将配置信息配置到webSecurity中
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
  1. 上述代码中T1标记处,我们看一下autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()的源代码
1
2
3
4
5
6
7
8
9
10
private final ConfigurableListableBeanFactory beanFactory;
public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() {
List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<SecurityConfigurer<Filter, WebSecurity>>();
Map<String, WebSecurityConfigurer> beansOfType = beanFactory
.getBeansOfType(WebSecurityConfigurer.class);
for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) {
webSecurityConfigurers.add(entry.getValue());
}
return webSecurityConfigurers;
}

这个beansOfType 就是我们定义的继承自WebSecurityConfigurerAdapter的类, 通过查看父类的定义,我们知道调用build()方法最后返回的必须是一个Filter对象,可以自行参考顶级父类(或接口)WebSecurityConfigurer和SecurityBuilder

springSecurityFilterChain()

为我们创建了一个名字叫做springSecurityFilterChain的Filter
源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filterchain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTR_NAME)
public Filter springSecurityFilterChain() throws Exception {
//T1 查看是否有WebSecurityConfigurer的相关配置
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
//T2 如果没有,说明我们没有注入继承WebSecurityConfigurerAdapter的对象(没有创建其子类)
if (!hasConfigurers) {

//T3 创建默认的配置信息WebSecurityConfigurerAdapter,保证SpringSecurity的
//最基础的功能,如果我们要有自定义的相关,一定要重配置
WebSecurityConfigurerAdapter adapter =objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
//T4 默认配置信息载入webSecurity
webSecurity.apply(adapter);
}
// T5这里build一个Filter
return webSecurity.build();
}

webSecurity对象在此时已经加载完所有的配置。

webSecurity对象为我们创建一个Filter通过的是build()方法。

注意: 建立了一个Filter对象,而这个Filter将会拦截掉我们的请求,对请求进行过滤拦截,从而起到对资源进行认证保护的作用。然后这个Filter并非我们自己平时定义的Filter这么简单,这个过滤器也只是一个代理的过滤器而已,里面还会有过滤器链。

WebSecurity的build()方法

WebSecurity继承了AbstractConfiguredSecurityBuilder类,实现了SecurityBuilder接口。

事实上AbstractConfiguredSecurityBuilder类的父类AbstractSecurityBuilder也是实现了SecurityBuilder接口。子类和父类实现同一个接口。事实上是为了子类在反射调用方法getInterfaces()中可以获取到接口,根据这里的情况就是WebSecurity反射调用getInterfaces()可以获取到SecurityBuilder接口。

WebSecurity 类图

KZtAW4.jpg

  • SecurityBuilder定义了构建的接口标准

  • AbstractSecurityBuilder实现build方法,用AtomicBoolean的变量building保证多线程情况下,操作的原子性。此处采用的是模板模式。定义了doBuild()抽象方法,用来给子类实现。

  • AbstractConfiguredSecurityBuilder 继承AbstractSecurityBuilder实现doBuild()方法,也采用模板模式,定义了实现的具体的步骤,如UNBUILT,INITIALIZING,CONFIGURING,BUILDING,以及BUILT。

我们看一下WebSecurity的定义

1
2
3
4
5
6
public final class WebSecurity extends
AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements
SecurityBuilder<Filter>, ApplicationContextAware {
...
...
}

从上面的代码可以看出WebSecurity指定泛型的类型为Filter,结合上面接口build()方法我们可以知道,WebSecurity的build()方法返回的是一个Filter,Spring Securiy 通过这个来创建一个过滤器。

build()过程

  • AbstractSecurityBuilder保证了线程的安全。
  • AbstractConfiguredSecurityBuilder保证了构建过程以及构建状态。
  • WebSecurity通过performBuild()来实现自身的构建

AbstractConfiguredSecurityBuilder的构建过程,我们看一下doBuild()方法的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING;

//默认什么都没做,WebSecurity也没有重写
beforeInit();
//T1默认此处调用WebSecurityConfigurerAdapter的init(finalWebSecurity web)方法
init();

buildState = BuildState.CONFIGURING;

//默认什么都不做,WebSecurity没有重写
beforeConfigure();
//调用WebSecurityConfigurerAdapter的configure(WbSecurity web),但是什么都没做
configure();

buildState = BuildState.BUILDING;
//T2这里调用WebSecurity的performBuild()方法
O result = performBuild();

buildState = BuildState.BUILT;

//从WebSecurity的实现,这里返回了一个Filter,完成构建过程
return result;
}
}

T1 处调用WebSecurityConfigurerAdapter的init(final WebSecurity web)方法,看一下源代码的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param web
* @throws Exception
*/
public void init(final WebSecurity web) throws Exception {
//构建HttpSecurity对象
final HttpSecurity http = getHttp();

//Ta
web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
public void run() {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
}
});
}
  • 这里构建了HttpSecurity对象,以及有一个共享对象FilterSecurityInterceptor。
  • Ta 处调用了WebSecurity的addSecurityFilterChainBuilder()方法,我们看一下这个方法的源代码
    1
    2
    3
    4
    5
    6
    public WebSecurity addSecurityFilterChainBuilder(
    SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
    //这个就是securityFilterChainBuilders变量
    this.securityFilterChainBuilders.add(securityFilterChainBuilder);
    return this;
    }
    在构建Filter过程的初始化的时候,我们对securityFilterChainBuilders这个变量进行了赋值,默认情况下securityFilterChainBuilders里面只有一个对象,那就是HttpSecurity。

T2 根据WebSecurity的属性构建Filter的performBuild()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Override
protected Filter performBuild() throws Exception {
Assert.state(
//T1 securityFilterChainBuilders哪里来的?
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
//T2 ignoredRequests.size()到底是什么?
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
//这个securityFilterChains 的集合里面存放的就是我们所有的过滤器链,根据长度的定义,
//我们也可以知道分为两种一个是通过 ignoredRequests 来的过滤器链,
//一个是通过 securityFilterChainBuilders 这个过滤器链构建链来的。
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
//如果是 ignoredRequest类型的,那么就添加默认过滤器链(DefaultSecurityFilterChain)
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}

//如果是securityFilterChainBuilder类型的,那么通过securityFilterChainBuilder的build()方法来构建过滤器链
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}

//将过滤器链交给一个过滤器链代理对象,而这个代理对象就是返回回去的
//过滤器。到这里为止,过滤器的过程已经结束
//T3 什么是FilterChainProxy?
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("\n\n"
+ "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result;
}
  1. WebSecurity的securityFilterChainBuilders属性哪里来的?
    见上边解释

  2. ignoredRequest是什么?
    ignoredRequests只是WebSecurity的一个属性

  3. ignoredRequests的list中的值从哪里来的呢?
    我们可以看到里面有一个ignore()方法,通过这个来进行设置的
    怎么设置呢,那我们得看看WebSecurityConfigurerAdapter这个类了,里面有一个configure方法供我们重写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public void configure(WebSecurity web) throws Exception {
    }
    #具体的例子如下
    @Override
    public void configure(WebSecurity web) throws Exception {
    super.configure(web);
    web.ignoring()
    .mvcMatchers("/favicon.ico", "/webjars/**", "/css/**");
    }

    然后值得一提的是这里有多少个mvcMatchers就会创建多少个ignoredRequests的对象,也就会有多少个过滤器链,也是在WebSecurity里面定义的内部类IgnoredRequestConfigurer这个类里面。

  4. FilterChainProxy到底是什么
    上面的描述中我们知道, FilterChainProxy是真正返回的Filter,上面代码中 FilterChainProxy的对象创建的源码为:

    1
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);

Filter实现的流程

KZBVNq.png

小结

SpringSecurityFilterChain 是Spring Security认证的入口。集成Spring Boot集成之后,xml配置被java注解配置取代,也就是 在WebSecurityConfiguration中完成了声明springSecurityFilterChain的作用。 并且最终交给DelegatingFilterProxy这个代理类,负责拦截请求。

也就是说:**@EnableWebSecurity完成的工作便是加载了WebSecurityConfiguration,AuthenticationConfiguration这两个核心配置类,也就此将spring security的职责划分为了配置安全信息,配置认证信息两部分。**

入手 WebSecurityConfigurerAdapter 适配器类。

  • HttpSecurity 通过getHttp()获取,后面会详细说到这个类
  • UserDetailsService 用户信息获取
  • AuthenticationManager 认证管理类
  • SecurityContextHolder

SecurityContextHolder 用于存储安全上下文信息(如操作用户是谁、用户是否被认证、用户权限有哪些),它用 ThreadLocal 来保存 SecurityContext,者意味着 Spring Security 在用户登录时自动绑定到当前现场,用户退出时,自动清除当前线程认证信息,SecurityContext 中含有正在访问系统用户的详细信息

导入依赖

导入 spring-boot-starter-security 依赖,在 SpringBoot 2.1 环境下默认使用的是 5.0 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<parent>
<groupId>org.springframework.boot</grupId>
<artifactId>spring-boot-starter-paren</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parentfrom repository -->
</parent>
<groupId>com.jelly</groupId>
<artifactId>spring-boot-security</artifacId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-security</name>
<description>Demo project for SpringBoot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot/groupId>
<artifactId>spring-boot-starter-scurity</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot/groupId>
<artifactId>spring-boot-starter-wb</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java<artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot<groupId>
<artifactId>mybatis-spring-boot-sarter</artifactId>
<version>1.2.0</version>
</dependency>
<!--https://mvnrepository.com/artifact/or.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupd>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot/groupId>
<artifactId>spring-boot-starter-tst</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.secuity</groupId>
<artifactId>spring-security-test<artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.oot</groupId>
<artifactId>spring-boot-mavenplugin</artifactId>
</plugin>
</plugins>
</build>

创建数据库

一般权限控制有三层,即:用户<–>角色<–>权限,用户与角色是多对多,角色和权限也是多对多。这里我们先暂时不考虑权限,只考虑用户<–>角色。

创建用户表sys_user:

1
2
3
4
5
6
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`password` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建权限表sys_role:

1
2
3
4
5
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

创建用户-角色表sys_user_role:

1
2
3
4
5
6
7
8
CREATE TABLE `sys_user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `fk_role_id` (`role_id`),
CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

初始化一下数据:

1
2
3
4
5
6
INSERT INTO `sys_role` VALUES ('1', 'ROLE_ADMIN');
INSERT INTO `sys_role` VALUES ('2', 'ROLE_USER');
INSERT INTO `sys_user` VALUES ('1', 'admin', '123');
INSERT INTO `sys_user` VALUES ('2', 'jitwxs', '123');
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');

注意: 这里的权限格式为 ROLE_XXX ,是Spring Security规定的,不要乱起名字哦。


准备页面

因为是示例程序,页面越简单越好,只用于登陆的login.html以及用于登陆成功后的home.html,将其放置在 resources/static 目录下:

login.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login">
<div>
用户名:<input type="text" name="username">
</div>
<div>
密码:<input type="password" name="password">
</div>
<div>
<button type="submit">立即登陆</button>
</div>
</form>
</body>
</html>

注意: 用户的登陆认证是由Spring Security进行处理的,请求路径默认为/login,用户名字段默认为username,密码字段默认为password

home.html

1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登陆成功</h1>
<a href="/admin">检测ROLE_ADMIN角色</a>
<a href="/user">检测ROLE_USER角色</a>
<button onclick="window.location.href='/logout'">退出登录</button>
</body>
</html>

配置application.properties

在配置文件中配置下数据库连接:

数据源

1
2
3
4
5
6
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/study?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
#开启Mybatis下划线命名转驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true

创建实体、Dao、Service和Controller

实体

SysUser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.jelly.security.bean;
import lombok.Data;
import java.io.Serializable;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 18:53
**/
@Data
public class SysUser {
private Integer id;
private String name;
private String password;
}

SysRole

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.jelly.security.bean;
import lombok.Data;
import java.io.Serializable;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 18:54
**/
@Data
public class SysRole {
private Integer id;
private String name;
}

SysUserRole

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.jelly.security.bean;
import lombok.Data;
import java.io.Serializable;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 18:55
**/
@Data
public class SysUserRole {
private Integer userId;
private Integer roleId;
}

Dao

SysUserMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.jelly.security.dao;
import com.jelly.security.bean.SysUser;
import org.apache.ibatis.annotations.Select;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:03
**/
public interface SysUserMapper {
@Select("SELECT * FROM sys_user WHERE id = #{id}")
SysUser selectById(Integer id);
@Select("SELECT * FROM sys_user WHERE name = #{name}")
SysUser selectByName(String name);
}

SysRoleMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.jelly.security.dao;
import com.jelly.security.bean.SysRole;
import org.apache.ibatis.annotations.Select;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:07
**/
public interface SysRoleMapper {
@Select("SELECT * FROM sys_role WHERE id = #{id}")
SysRole selectById(Integer id);
}

SysUserRoleMapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.jelly.security.dao;
import com.jelly.security.bean.SysUserRole;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:08
**/
public interface SysUserRoleMapper {
@Select("SELECT * FROM sys_user_role WHERE user_id = #{userId}")
List<SysUserRole> listByUserId(Integer userId);
}

Service

SysUserService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.jelly.security.service;
import com.jelly.security.bean.SysUser;
import com.jelly.security.dao.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:09
**/
@Service
public class SysUserService {
@Autowired
private SysUserMapper userMapper;
public SysUser selectById(Integer id) {
return userMapper.selectById(id);
}
public SysUser selectByName(String name) {
return userMapper.selectByName(name);
}
}

SysRoleService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.jelly.security.service;
import com.jelly.security.bean.SysRole;
import com.jelly.security.dao.SysRoleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:10
**/
@Service
public class SysRoleService {
@Autowired
private SysRoleMapper roleMapper;
public SysRole selectById(Integer id){
return roleMapper.selectById(id);
}
}

SysUserRoleService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.jelly.security.service;
import com.jelly.security.bean.SysUserRole;
import com.jelly.security.dao.SysUserRoleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:11
**/
@Service
public class SysUserRoleService {
@Autowired
private SysUserRoleMapper userRoleMapper;
public List<SysUserRole> listByUserId(Integer userId) {
return userRoleMapper.listByUserId(userId);
}
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.jelly.security.web;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:12
**/
@Controller
public class LoginController {
private Logger logger = LoggerFactory.getLogger(LoginController.class);
@RequestMapping("/")
public String showHome() {
String name = SecurityContextHolder.getContext().getAuthentication().getName();
logger.info("当前登陆用户:" + name);
return "home.html";
}
@RequestMapping("/login")
public String showLogin() {
return "login.html";
}
@RequestMapping("/admin")
@ResponseBody
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String printAdmin() {
return "如果你看见这句话,说明你有ROLE_ADMIN角色";
}
@RequestMapping("/user")
@ResponseBody
@PreAuthorize("hasRole('ROLE_USER')")
public String printUser() {
return "如果你看见这句话,说明你有ROLE_USER角色";
}
}

如代码所示,获取当前登录用户: SecurityContextHolder.getContext().getAuthentication()
@PreAuthorize
用于判断用户是否有指定权限,没有就不能访问


配置SpringSecurity

UserDetailsService

首先我们需要自定义 UserDetailsService ,将用户信息和权限注入进来。

我们需要重写 loadUserByUsername 方法,参数是用户输入的用户名。返回值是UserDetails,这是一个接口,一般使用它的子类org.springframework.security.core.userdetails.User,它有三个参数,分别是用户名、密码和权限集。

实际情况下,大多将 DAO 中的 User 类继承 org.springframework.security.core.userdetails.User 返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package com.jelly.security.security;
import com.jelly.security.bean.SysRole;
import com.jelly.security.bean.SysUser;
import com.jelly.security.bean.SysUserRole;
import com.jelly.security.service.SysRoleService;
import com.jelly.security.service.SysUserRoleService;
import com.jelly.security.service.SysUserService;
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.Collection;
import java.util.List;
/**
* 我们需要自定义 UserDetailsService ,将用户信息和权限注入进来。
*
* 我们需要重写 loadUserByUsername 方法,参数是用户输入的用户名。
*
* 返回值是UserDetails,这是一个接口,一般使用它的子类o
*
* rg.springframework.security.core.userdetails.User,
*
* 它有三个参数,分别是用户名、密码和权限集。
* @version V1.0
* @author: Jelly
* @program: spring-boot-security
* @description:
* @date: 2019-03-10 19:25
**/
@Service("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Autowired
private SysUserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 从数据库中取出用户信息
SysUser user = userService.selectByName(username);
// 判断用户是否存在
if(user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
// 添加权限
List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
for (SysUserRole userRole : userRoles) {
SysRole role = roleService.selectById(userRole.getRoleId());
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
// 返回UserDetails实现类
return new User(user.getName(), user.getPassword(), authorities);
}
}

WebSecurityConfig

该类是 Spring Security 的配置类,该类的三个注解分别是标识该类是配置类、开启 Security 服务、开启全局 Securtiy 注解。

首先将我们自定义的 userDetailsService 注入进来,在 configure() 方法中使用 auth.userDetailsService() 方法替换掉默认的 userDetailsService。

这里我们还指定了密码的加密方式(5.0 版本强制要求设置),因为我们数据库是明文存储的,所以明文返回即可,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.jelly.security.security;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @version V1.0
* @author: Jelly
* @program:
* @description:
* @date: 2019-03-10 17:27
**/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
/**
* 想要密码加密
*
* auth.userDetailsService(userDetailsService)
* .passwordEncoder(new BCryptPasswordEncoder());
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
});
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允许匿名的url,填在下面
// .antMatchers().permitAll()
.anyRequest().authenticated()
.and()
// 设置登陆页
.formLogin().loginPage("/login")
// 设置登陆成功页
.defaultSuccessUrl("/").permitAll()
// 自定义登陆用户名和密码参数,默认为username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
.logout().permitAll();
// 关闭CSRF跨域
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers("/css/**", "/js/**");
}
}

运行程序

在启动类添加注解

1
@MapperScan("com.jelly.security.dao")

启动项目,浏览器输入:localhost:8080。有以下两个角色。

1
2
ROLE_ADMIN 账户:用户名 admin,密码 123
ROLE_USER 账户:用户名 jitwxs,密码 123

配置跨域

在configure中添加

1
.and().cors().

KtFCLR.png

下边添加:

1
2
3
4
5
6
7
8
9
10
11
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration cors = new CorsConfiguration();
cors.setAllowCredentials(true);
cors.addAllowedOrigin("*");
cors.addAllowedHeader("*");
cors.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors);
return new CorsFilter(urlBasedCorsConfigurationSource);
}