springboot 整合spring security

security作为一个流行的安全框架,很多公司都用其来做web的认证与授权。activiti7 工作流默认使用其的 用户-角色 功能。所以我们必须了解它。

建立web项目

首先建立一个web项目(springboot)同时我们写上一个接口用于测试。

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
@RestController
public class TestController {
private Logger logger = LoggerFactory.getLogger(TestController.class);
@GetMapping(value = "/admin/1")
public Object admin(){
Map map = new HashMap();
map.put("code",200);
map.put("msg","success");
map.put("data","admin");
return map;
}
@GetMapping("/user/1")
public Object user(){
Map map = new HashMap();
map.put("code",200);
map.put("msg","success");
map.put("data","user");
return map;
}
@GetMapping("/free/1")
public Object free(){
Map map = new HashMap();
map.put("code",200);
map.put("msg","success");
map.put("data","free");
return map;
}

@PostMapping("/logout/success")
public Object logoutSuccess(){
return "logout success POST";
}
@GetMapping("/logout/success")
public Object logoutSuccessGet(){
return "logout success Get";
}
@PostMapping("/login/error2")
public Object loginError(){
return "logoin error";
}
@GetMapping("/test")
public Object test(){
return "test";
}
}

启动项目访问我们的接口

(正常返回数据)

image-20200331163706253.png

添加security依赖

1
2
3
4
5
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

再次访问接口:

image-20200331164035818.png

接口提示401 msg显示没有认证 说明security已经其作用了

接着我们再用浏览器看看

http://localhost:8080/admin/1

image-20200331164253612.png

发现浏览器重定向到一个登录页面

从IDEA控制台我们发现密码巴拉巴拉什么的

image-20200331164557365.png

然后我们使用 用户名:user 还有控制台的密码登录及可返回我们想要的数据。

从spring security 的文档中也可以找到说明:

image-20200331165141402.png

当然这种用户名和密码我们也可以自定义:

1
2
spring.security.user.name=abc
spring.security.user.password=123457

简单配置与说明

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
70
71
72
73
74
75
76
77
78
package com.example.springbootactiviti.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* @ClassName SpringSecurityCustomConfig
* @Author wuzhiyong
* @Date 2020/3/4 21:17
* @Version 1.0
**/
@EnableWebSecurity
public class SpringSecurityCustomConfig extends WebSecurityConfigurerAdapter {

@Autowired
private PasswordEncoder encoding;
// @Autowired
// private SpringDataUserDetailsService userDetailsService;

/**
* 指定加密方式(默认会自动配上BCrypt加密)
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
// return new Md4PasswordEncoder();
//配置不加密的时候 下方的用户的密码用明文即可
// return NoOpPasswordEncoder.getInstance(); //不加密
}

// @Bean
// public SpringDataUserDetailsService customUserDetailsService() {
// return new SpringDataUserDetailsService();
// }

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
// .userDetailsService(userDetailsService)//配置自定义的验证(从数据库查询)逻辑
.inMemoryAuthentication()
.withUser("user").password(encoding.encode("123")).roles("USER").and()
.withUser("admin").password(encoding.encode("1234")).roles("USER", "ADMIN")
.and().withUser("zhnagsan").password(encoding.encode("12345")).roles("LEADER")
;
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
//关闭 CSRF 保护(如果不关闭 访问logoutUrl 必须为post方式 见源码注释)
.csrf().disable()
.authorizeRequests()
//允许匿名访问
.antMatchers("/free/**","/logout/success").permitAll()
//匹配/admin/** API需要ADMIN的角色
.antMatchers("/admin/**").hasRole("ADMIN")
//拥有其中任意权限
.antMatchers("/user/**").hasAnyAuthority("ROLE_USER","ROLE_ADMIN")
//拥有其中任意权限(与上面不同的是这里不用 ROLE_前缀)
.antMatchers("/test").hasAnyRole("USER","ADMIN")
//任何请求都需要认证
.anyRequest().authenticated()
.and()
//配置登录页面,登录错误后的路径(同样是登录的页面)
.formLogin().failureUrl( "/login/error2" )
//配置登出的 url 以及 登出成功后 重定向的url地址
//注意:logoutSuccessUrl url 必须是 能够匿名访问的 否则 重定向过去后 会被拦截 再跳转到登录页面
.and().logout().logoutUrl("/logout/out").logoutSuccessUrl("/logout/success")
;
}
}

在配置权限认证的时候,遵循自上而下的匹配规则。

上面我们的用户是在代码中写死的,而实际项目中我们的用户信息都是在数据库里接下来我们来实现从数据库里读取用户的逻辑。

新建一个类:

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
package com.example.springbootactiviti.demo.config;

import com.example.springbootactiviti.demo.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.ArrayList;
import java.util.List;
/**
* @ClassName SpringDataUserDetailsService
* @Author wuzhiyong
* @Date 2020/3/6 12:37
* @Version 1.0
**/
public class SpringDataUserDetailsService implements UserDetailsService {

@Autowired
JdbcTemplate jdbcTemplate;

/**
* 需新建配置类注册一个指定的加密方式Bean,或在下一步Security配置类中注册指定
*/
@Autowired
private PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 通过用户名从数据库获取用户信息
// User userInfo = userInfoService.queryForObject("select * fron user", com.example.springbootactiviti.demo.model.User.class);
List<User> userList = jdbcTemplate.query("select * from user where username = ?",new Object[]{username}, new BeanPropertyRowMapper<>(User.class));

if (userList == null) {
throw new UsernameNotFoundException("用户不存在");
}
userList.forEach(i-> System.out.println(i.toString()));
// 得到用户角色
String role = userList.get(0).getRole();

// 角色集合
List<GrantedAuthority> authorities = new ArrayList<>();
// 角色必须以`ROLE_`开头,数据库中没有,则在这里加
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));

return new org.springframework.security.core.userdetails.User(
userList.get(0).getUsername(),
// 如果数据库是明文,这里需加密,除非指定配置了不加密(一般数据库里存的都是加密后的字符串)
userList.get(0).getPassword(),
authorities
);
}
}

实体类:

1
2
3
4
5
6
7
8
9
10
public class User {
private String id;

private String username;

private String password;

private String role;
}
//set get 方法就省略不贴了

准备数据:

其中user密码明文是123 ,admin 密码明文是1234.这里插入的都是BCrypt加密后的密文。

1
2
3
4
5
6
7
8
9
10
11
create table user
(
id varchar(20) not null,
username varchar(50) not null,
password varchar(100) not null,
role varchar(50) not null
)
collate = utf8_bin;

INSERT INTO spring_security.user (id, username, password, role) VALUES ('1', 'user', '$2a$10$elDLIbuSf9UZ9XpLr2FVPOgfAQARQURnbymSg7HyxCTW.copZR3Y6', 'USER');
INSERT INTO spring_security.user (id, username, password, role) VALUES ('2', 'admin', '$2a$10$P0mwGYvKDgK5KBr7ybQ7D.GJJ4Ban3wSB/1DvOo17qjktvgH5Pwh6', 'ADMIN');

修改配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Autowired
private SpringDataUserDetailsService userDetailsService;

@Bean
public SpringDataUserDetailsService customUserDetailsService() {
return new SpringDataUserDetailsService();
}

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)//配置自定义的验证(从数据库查询)逻辑
// .inMemoryAuthentication()
// .withUser("user").password(encoding.encode("123")).roles("USER").and()
// .withUser("admin").password(encoding.encode("1234")).roles("USER", "ADMIN")
// .and().withUser("zhnagsan").password(encoding.encode("12345")).roles("LEADER")
;
}

好了!如果是一般的单体架构web项目,了解这些基本满足使用了。

由于restful API 与分布式、微服务的流行,现在很多项目都已经实现了前后端的分离。服务端(后端)只提供调用方API,前后端采用token认证的方式进行数据交互。而在token认证方式中 jwt 是比较流行的一种。接下来开始spring security 与 jwt 的整合。

security+JWT

首先简单说下token认证授权的逻辑3步走

  1. 用户通过名称和密码访问登录接口 登录成功返回 token
  2. 用户带上token(一般存放在head中) 访问 资源地址 服务端拦截请求解析token 如果正确则返回资源
  3. 用户访问登出接口 服务端清除token

再简单认识下security中的常见的过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//可简单认为是  登录前置 的过滤器
UsernamePasswordAuthenticationFilter

//可简单认为是 认证与授权的 过滤器
BasicAuthenticationFilter

//对用户名密码的一层封装
UsernamePasswordAuthenticationToken

//认证对象的封装 也是UsernamePasswordAuthenticationToken的顶级父类
Authentication

//security 中对用户的抽象 通常业务中的User类继承自此类
UserDetails

//通过传入的名称 返回UserDetails 对象 相当于userDao,在这里可以自定从不同的数据源里获取所需的对象数据
UserDetailsService

//认证管理
authenticationManager

security 默认的 /login(可配置) 作为登录的url 。当访问该login时。会被

AbstractAuthenticationProcessingFilter拦截后调用UsernamePasswordAuthenticationFilter 的方法得到UsernamePasswordAuthenticationToken做认证。然后调用过滤器链继续过滤

其中UsernamePasswordAuthenticationFilter 里是将读取用户名密码等信息封装成UsernamePasswordAuthenticationToken

当访问需要某项权限的url时(/login 接口不需要权限)会经过BasicAuthenticationFilter过滤器时会进行认证授权。并把认证结果保存在上下文中。

在BasicAuthenticationFilter中这里会在request中解析(通过存在请求头中的内容)出UsernamePasswordAuthenticationToken委托给authenticationManager的authenticate方法进行认证。

在authenticate的具体实现中会取出前面解析出的UsernamePasswordAuthenticationToken 中的用户名

通过UserDetailsService获取到数据库的用户信息(密码)与UsernamePasswordAuthenticationToken 中的密码进行比对。如果发生异常则抛出。

抛出的异常会被BasicAuthenticationFilter捕获 并交给认证失败处理方法(可重写)进行后续处理。

如果没有异常就继续交给过滤器链过滤处理。

还记得上面的4步走么

其中第一步

我们可以手动写一个restful接口用于登录。接口内验证用户名密码无误后通过jwt工具生成一个token返回给客户端。也可以继承自UsernamePasswordAuthenticationFilter 类在请求中拿到用户名密码验证无误后返回token给客户端。(返回客户端前 可把token存入redis 等)

其中第二步

用户访问资源链接时带上token会被BasicAuthenticationFilter拦截。

我们自定义一个类继承与它。把请求头中的token用jwt工具类解析然后封装成UsernamePasswordAuthenticationToken 其它代码不变。(当然如果前面把相关信息存在了redis里这里可直接在redis里取)

其中第三步

我们可以写个restful接口,客户端访问后,我们通过token解析出用户后,清除token(如果是redis做token验证这里清空redis里的token即可,如果不是可通过jwt工具类设置token的时效性使其失效)

补充说明

由于前后端的交互统一的json格式。所以我们需要重写掉security验证失败的默认处理unsuccessfulAuthentication方法。

很多地方是通过AuthenticationFailureHandler类来做默认处理的这里我们继承重写掉这个类即可

补充

1
2
3
4
5
6
7
8
9
extends
UsernamePasswordAuthenticationFilter
BasicAuthenticationFilter
AbstractSecurityInterceptor
OncePerRequestFilter
AbstractAuthenticationProcessingFilter
implements
FilterInvocationSecurityMetadataSource
AccessDecisionManager