spring security实践

官网操作及阅读

https://spring.io/

点击 projects 选择 spring security

首先看到的overview下有简介和特色或者功能还有快速开始。

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求。

特征:

​ 对身份验证和授权的全面且可扩展的支持

​ 防止会话固定、点击劫持、跨站点请求伪造等攻击

​ Servlet API 集成

​ 与 Spring Web MVC 的可选集成

入门分为:

入门(Servlet)

入门 (WebFlux)

入门(servlet)

可以点击下载最小的 Spring Boot + Spring Security 应用程序。

这个项目其实就是创建spring boot 添加 下面的依赖

1
2
3
4
5
6
7
8
9
10
    <properties>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
@RestController
public class IndexController {

@GetMapping("/")
public String index() {
return "success.....";
}

}

这时启动spring boot 项目

可以直接输入http://localhost:8080/login 会跳转到springsecurity提供的登录页。也可以直接访问http://localhost:8080/也会自动跳转。

根据官网提示,它会创建一个用户名为user的用户,密码在启动日志打印里。

登录成功后会自动跳转到http://localhost:8080/显示sucess

读取用户名和密码的方式(表单、基本、摘要)

表单 (默认)

默认配置相当于下面的配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults())
.authorizeRequests().anyRequest().authenticated();
}

}

测试表单同上。

基本

httpBasic是由http协议定义的最基础的认证方式。每次请求时,在请求头Authorization参数中附带用户/密码的base64编码,参考base64。这个方式并不安全,不适合在web项目中使用。但它是一些现代主流认证的基础,而且在spring security的oauth中,内部认证默认就是用的httpBasic。

配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic(Customizer.withDefaults())
.authorizeRequests().anyRequest().authenticated();
}

}

这时使用浏览器访问,不再会跳转到登录页,而会弹出浏览器的用户名密码窗口。

在调试窗口中查看该请求的响应头,其中有个WWW-Authenticate: Basic realm=”Realm”。

WWW-Authenticate:服务器告知浏览器代理认证工作。

Basic:认证类型为Basic。

realm=”Realm”:认证域名为Realm

realm
realm=”Realm”:指认证域名为Realm。在未认证用户请求不同的接口时,后台根据给该接口分配的域,可以响应不同的realm名,并且用不同用户名/密码进行认证。所以用户每请求一个新Realm的url,都会弹框要求用新Realm的用户名/密码进行认证,就好比不同的角色登录只能请求属于该角色的url。httpBasic默认realm名为Realm,可以用以下方式配置。

1
http.httpBasic().realmName("Realm")

此时的响应码为401,根据401和以上响应头,浏览器会接管工作,它会弹出上面那个框要求输入用户名/密码,并将其拼接成“用户名:密码”格式,中间是一个冒号,再用base64编码成xxx,然后在请求头中附加Authorization:Basic xxx,发给后台认证。后台需要用base64解码xxx,再认证用户名/密码。

认证错误:浏览器会保持弹框。

认证成功:浏览器会缓存有效的base64编码,在之后的请求中,浏览器都会在请求头中添加有效编码。
可以使用浏览器的清除 cookies及其他网站数据清除该用户名密码。

摘要认证

您不应该在现代应用程序中使用摘要式身份验证,因为它不被认为是安全的。最明显的问题是您必须以明文、加密或 MD5 格式存储密码。所有这些存储格式都被认为是不安全的。相反,您应该使用 Digest Authentication 不支持的单向自适应密码散列(即 bCrypt、PBKDF2、SCrypt 等)存储凭据。

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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}

@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated().and()
.addFilter(digestAuthenticationFilter())//在过滤链中添加摘要认证过滤器
.exceptionHandling().accessDeniedPage("/403")
.authenticationEntryPoint(digestAuthenticationEntryPoint())//摘要认证入口端点
.and()
.csrf().disable();
}

@Bean
public DigestAuthenticationFilter digestAuthenticationFilter(){
DigestAuthenticationFilter filter= new DigestAuthenticationFilter();
filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());//必须配置
filter.setUserDetailsService(userDetailsService());//必须配置
return filter;
}
@Bean
public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
point.setRealmName("realm");//realm名
point.setKey("key");//密钥
return point;
}
@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsService() {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//省略从数据库查询过程
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
authorities.add(new SimpleGrantedAuthority("auth"));
return new User(username, "123", true, true, true, true, authorities);
}
};
}

}

测试同http基本认证

自定义登录页面(认证还是默认的)

formLogin自动配置了一些url和页面:

/login (get):登录页面,任意没有登录的请求都会跳转到这里,就是上面看到的那个页面。
/login (post):登录接口,在登录页面点击登录,会请求这个接口。
/login?error:用户名或密码错误,跳转到该页面。
/:登录成功后,默认跳转的页面,/会重定向到index.html,这个页面要你自己实现。
/logout:注销页面。
/login?logout:注销成功跳转页面。

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>2.6.7</version>
</dependency>

添加templates/login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
<head>
<title>Please Log In</title>
</head>
<body>
<h1>Please Log In</h1>
<div th:if="${param.error}">
Invalid username and password.</div>
<div th:if="${param.logout}">
You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<input type="text" name="username" placeholder="Username"/>
</div>
<div>
<input type="password" name="password" placeholder="Password"/>
</div>
<input type="submit" value="Log in" />
</form>
</body>
</html>

添加跳转的controller

1
2
3
4
5
6
7
@Controller
class LoginController {
@GetMapping("/login")
String login() {
return "login";
}
}

添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置为表单登录及登录页面
http.formLogin().loginPage("/login")
.and().authorizeRequests()
// 登录页可以访问
.antMatchers("/login").permitAll()
// 其他都需要认证
.anyRequest().authenticated();
}
}

密码存储

密码编码器(PasswordEncoder)

不管密码存储到哪都需要密码编码器,因为明文密码是不安全的。

spring security提供了很多实现类

这些实现类都以算法名开头 XXXPasswordEncoder

1
2
3
4
5
6
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("123"); //注册时,对密码123加密,结果将保存到数据库
System.out.println(result);
encoder.matches("123", result); //登录时,从数据库读取密文,验证密码是否是123
}

spring security 提供了四种方式

把密码存储到内存中

这里使用的是默认页面,内存里存储用户名和密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public UserDetailsService users() {
UserDetails user = User.builder()
.username("user")
.password("$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy")
.roles("USER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}

}

测试访问 http://localhost:8080/ 自动跳转到登录页,输入用户名user 密码 123,发现跳转“/页面。

兼容多种密码解码器

1
2
3
4
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{bcrypt}$2a$10$89rbg/..2V1hoRzXtlDa9ejjIzsqeO.kQgmkQnAa//xDzJLoyJgAu").authorities("auth").and()
.withUser("old").password("{noop}123").authorities("old");

此时不需要再设置passwordEncoder,而是在密码中加上前缀进行区分。如以前的用户old,给其密码加上前缀{noop},表示未加密。新用户密码前缀为{bcrypt},表示bcrypt加密。系统为根据前缀自动识别你的加密方式。在自认定认证中,同样可以根据前缀判断加密方式。

把密码存储到jdbc关系数据库中

为了简单演示,使用嵌入式数据库H2

添加依赖

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.212</version>
</dependency>

复制ddl文件到你的resources目录中,这个文件是创建表的sql

目录在spring security的core包的org/springframework/security/core/userdetails/jdbc/users.ddl

1
2
3
create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null);
create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);

配置

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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(H2)
.addScript("users.ddl")
.build();
}

@Bean
public UserDetailsService users(DataSource dataSource) {
UserDetails user = User.builder()
.username("user")
.password("$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy")
.roles("USER")
.build();
UserDetails admin = User.builder()
.username("admin")
.password("$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy")
.roles("USER", "ADMIN")
.build();
JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
users.createUser(user);
users.createUser(admin);
return users;
}

}

测试同理。

LDAP存储

使用嵌入式的ldap

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>4.0.14</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
<type>jar</type>
<scope>compile</scope>
</dependency>

添加配置文件users.ldif

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
dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: $2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: $2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin

配置

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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
UnboundIdContainer ldapContainer() {
return new UnboundIdContainer("dc=springframework,dc=org",
"classpath:users.ldif");
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.groupSearchBase("ou=groups")
.contextSource()
.url("ldap://localhost:53389/dc=springframework,dc=org")
.and()
.passwordCompare()
.passwordEncoder(passwordEncoder())
.passwordAttribute("userPassword")
.and()
.ldapAuthoritiesPopulator(new LdapAuthoritiesPopulator() {
@Override
public Collection<? extends GrantedAuthority> getGrantedAuthorities(DirContextOperations userData, String username) {
return Collections.emptyList();
}
});
}

}

测试同上。

自定义数据存储(一般使用这个)

添加UserDetailsService实现类

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库获取用户
if (!username.equals("admin")) {
throw new UsernameNotFoundException("用户不存在");
}
return new User(username, "$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy", AuthorityUtils.commaSeparatedStringToAuthorityList("admin1, admin2"));
}

}

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

并发会话控制(只在一个账号登录只存在一台设备上)

(1)踢掉前面的登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults())
.authorizeRequests().anyRequest().authenticated();
http.sessionManagement().maximumSessions(1);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

(2)禁止新的登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults())
.authorizeRequests().anyRequest().authenticated();
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

记住我

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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin(Customizer.withDefaults())
.authorizeRequests().anyRequest().authenticated();

http.rememberMe().userDetailsService(customUserDetailsService).tokenValiditySeconds(3600);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

登录时选择remember me ,登录后查看cookies会有一个remember me 的cookies,这时关闭浏览器不用重新登录。

登出

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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll();
http.logout().logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出成功");
}
});
http.authorizeRequests().anyRequest().authenticated();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

还是不能自定义退出接口

无权限403错误处理

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
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll();
http.logout().logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出成功");
}
});
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("处理无权限错误返回JSON");
PrintWriter printWriter = response.getWriter();
printWriter.println("处理无权限错误返回JSON");
printWriter.flush();
printWriter.close();
}
});
http.authorizeRequests().antMatchers("/login/success").hasAnyRole("admin");
http.authorizeRequests().anyRequest().authenticated();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

授权

http.authorizeRequests() 就是授权。

hasRole(String role) 返回true当前主体是否具有指定角色。例如,hasRole('admin')默认情况下,如果提供的角色不以“ROLE_”开头,它将被添加。这可以通过修改defaultRolePrefixon来定制DefaultWebSecurityExpressionHandler
hasAnyRole(String… roles) 返回true当前主体是否具有任何提供的角色(以逗号分隔的字符串列表形式给出)。例如,hasAnyRole('admin', 'user')默认情况下,如果提供的角色不以“ROLE_”开头,它将被添加。这可以通过修改defaultRolePrefixon来定制DefaultWebSecurityExpressionHandler
hasAuthority(String authority) true如果当前委托人具有指定的权限,则返回。例如,hasAuthority('read')
hasAnyAuthority(String… authorities) 返回true当前主体是否具有任何提供的权限(以逗号分隔的字符串列表形式给出)例如,hasAnyAuthority('read', 'write')
principal 允许直接访问代表当前用户的主体对象
authentication 允许直接访问AuthenticationSecurityContext
permitAll 总是评估为true
denyAll 总是评估为false
isAnonymous() 返回true当前主体是否为匿名用户
isRememberMe() true如果当前主体是记住我的用户,则返回
isAuthenticated() true如果用户不是匿名的,则返回
isFullyAuthenticated() true如果用户不是匿名用户或记住我的用户,则返回
hasPermission(Object target, Object permission) 返回true用户是否有权访问给定权限的目标。例如,hasPermission(domainObject, 'read')
hasPermission(Object targetId, String targetType, Object permission) 返回true用户是否有权访问给定权限的目标。例如,hasPermission(1, 'com.example.domain.Message', 'read')

有四个注释支持表达式属性,以允许调用前和调用后的授权检查,还支持过滤提交的集合参数或返回值。它们是@PreAuthorize、@PreFilter和@PostAuthorize、@PostFilter。

自定义权限验证bean

自定义验证bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class WebSecurity {

public boolean check(String permission) {
// 获取用户的权限
Set<String> permissions = new HashSet<>();
// 自定义 模块 用户 添加功能
permissions.add("userManage:user:add");
if (permissions.contains(permission)) {
return true;
}
return false;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@RestController
public class IndexController {

@PreAuthorize("@webSecurity.check('userManage:user:add')")
@GetMapping("/")
public String index() {
return "success.....";
}

@PreAuthorize("@webSecurity.check('userManage:user:aa')")
@GetMapping("/index1")
public String index1() {
return "index1.....";
}

@GetMapping("/error")
public String error() {
return "error.....";
}


}
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
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().permitAll();
http.logout().logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出成功");
}
});
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
System.out.println("处理无权限错误返回JSON");
PrintWriter printWriter = response.getWriter();
printWriter.println("处理无权限错误返回JSON");
printWriter.flush();
printWriter.close();
}
});
http.authorizeRequests().anyRequest().authenticated();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
CustomUserDetailsService customUserDetailsService() {
return new CustomUserDetailsService();
}

}

使用默认的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库获取用户
if (!username.equals("admin")) {
throw new UsernameNotFoundException("用户不存在");
}
Set<GrantedAuthority> authoritySet = new HashSet<>();
SimpleGrantedAuthority role = new SimpleGrantedAuthority("ROLE_admin1");
authoritySet.add(role);
return new User(username, "$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy", authoritySet);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class LoginController {

@PreAuthorize("hasRole('admin1')")
@GetMapping("/login/{status}")
public String login(@PathVariable String status) {
System.out.println(status);
if ("auth".equals(status)) {
return "没有登录";
}
if ("fail".equals(status)) {
return "登录失败";
}
if ("success".equals(status)) {
return "登录成功";
}
if ("logout".equals(status)) {
return "注销成功";
}
return "";
}
}

整合JWT

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class CustomUserDetailsService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库获取用户
if (!username.equals("admin")) {
throw new UsernameNotFoundException("用户不存在");
}
return new User(username, "$2a$10$euvsO7nBJjnyRgtgLG7EfurLQD2cql6lHPMewpDxp7v8p2i0NZDVy", AuthorityUtils.commaSeparatedStringToAuthorityList("admin1, admin2"));
}

}
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
@RestController
public class LoginController {

@Autowired
private AuthenticationManager authenticationManager;

@GetMapping(value = "/login")
public String Login(@RequestParam("username") String username,
@RequestParam("password") String password) {
// 用户验证
Authentication authentication = null;
try
{
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
}
catch (Exception e)
{
if (e instanceof BadCredentialsException)
{
throw new RuntimeException(e.getMessage());
}
else
{
throw new RuntimeException(e.getMessage());
}
}
Map<String, String> claimMap = new HashMap<>();
claimMap.put("username", "abc");
return TokenUtli.GenerateToken(claimMap);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class IndexController {

@GetMapping("/")
public String index() {
return "success.....";
}

@GetMapping("/error")
public String error() {
return "error.....";
}


}
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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

public class TokenUtli {

//Issuer
public static final String ISSUER = "auth0";
//Audience
public static final String AUDIENCE = "Client";
//密钥
public static final String KEY = "123456";
//算法
public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY);
//Header
public static final Map<String, Object> HEADER_MAP = new HashMap<String, Object>() {
{
put("alg", "HS256");
put("typ", "JWT");
}
};

/**
* 生成 Token 字符串
*
* @param claimMap claim 数据
* @return Token 字符串
*/
public static String GenerateToken(Map<String, String> claimMap) {
Date nowDate = new Date();
//120 分钟过期
Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);

//Token 建造器
JWTCreator.Builder tokenBuilder = JWT.create();

for (Map.Entry<String, String> entry : claimMap.entrySet()) {
//Payload 部分,根据需求添加
tokenBuilder.withClaim(entry.getKey(), entry.getValue());
}

//token 字符串
String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP)//Header 部分
.withIssuer(TokenUtli.ISSUER)//issuer
.withAudience(TokenUtli.AUDIENCE)//audience
.withIssuedAt(nowDate)//生效时间
.withExpiresAt(expireDate)//过期时间
.sign(TokenUtli.ALGORITHM);//签名,算法加密

return token;
}

/**
* 时间加法
*
* @param date 当前时间
* @param minute 持续时间(分钟)
* @return 时间加法结果
*/
private static Date AddDate(Date date, Integer minute) {
if (null == date) {
date = new Date();
}
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minute);

return calendar.getTime();
}

/**
* 验证 Token
*
* @param webToken 前端传递的 Token 字符串
* @return Token 字符串是否正确
* @throws Exception 异常信息
*/
public static boolean VerifyJWTToken(String webToken) throws Exception {
if (StringUtils.isEmpty(webToken)) {
return false;
}
String[] token = webToken.split(" ");
if (token.length <= 1) {
throw new Exception("token错误");
}
if (token[1].equals("")) {
throw new Exception("token错误");
}


//JWT验证器
JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();

//解码
DecodedJWT jwt = verifier.verify(token[1]);//如果 token 过期,此处就会抛出异常

//Audience
List<String> audienceList = jwt.getAudience();
String audience = audienceList.get(0);

//Payload
Map<String, Claim> claimMap = jwt.getClaims();
for (Map.Entry<String, Claim> entry : claimMap.entrySet()) {

}

//生效时间
Date issueTime = jwt.getIssuedAt();
//过期时间
Date expiresTime = jwt.getExpiresAt();

return true;
}

public static Map<String, Claim> parseToken(String webToken) throws Exception {
if (StringUtils.isEmpty(webToken)) {
return null;
}
String[] token = webToken.split(" ");
if (token.length <= 1) {
throw new Exception("token错误");
}
if (token[1].equals("")) {
throw new Exception("token错误");
}
//JWT验证器
JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();

//解码
DecodedJWT jwt = verifier.verify(token[1]);//如果 token 过期,此处就会抛出异常
return jwt.getClaims();
}

}

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
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
String token = getToken(request);
try {
Map<String, Claim> map = TokenUtli.parseToken(token);
if (map != null && map.get("username") != null) {
String username = map.get("username").asString();
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, null);
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
} catch (Exception e) {
e.printStackTrace();
System.out.println("报错了");
}
}

private String getToken(HttpServletRequest request)
{
String token = request.getHeader("Authorization");
return token;
}
}
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
EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private CustomUserDetailsService customUserDetailsService;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF禁用,因为不使用session
http.csrf().disable();
// 基于token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
http.authorizeRequests().antMatchers("/login").permitAll();
http.authorizeRequests().anyRequest().authenticated();
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

}

/**
* 身份认证接口
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder);
}


}

rouyi配置详解

还是看rouyi项目吧


spring security实践
http://hanqichuan.com/2022/05/20/权限框架/spring security实践/
作者
韩启川
发布于
2022年5月20日
许可协议