Spring Security学习笔记
Spring Security学习笔记
1 入门项目
- pom依赖:
- 配置默认账户和密码
- HTTP基本认证;
- Form表单认证
- HTTP摘要认证;
- 认证步骤:
- 客户端首先发起一个未携带认证信息的请求;
-
然后服务器端返回一个401 Unauthorized的响应信息,并在
WWW-Authentication
头部中说明认证形式:当进行HTTP基本认证时,WWW-Authentication会被设置为Basic realm=“被保护的页面”
; - 接下来客户端会收到这个401 Unauthorized响应信息,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码使用冒号进行拼接并用Base64编码,然后将其放入到请求的Authorization头部并发送给服务器;
- 最后服务器端对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容。
-
anyRequest()
:表示匹配所有的url请求 -
antMatcher()
:传递一个ant表达式参数,表示匹配所有满足ant表达式的请求。-
?
:匹配一个字符 -
*
:匹配0个或多个字符 -
**
:匹配0个或多个目录 -
permitAll()
:表示所匹配的URL任何人都允许访问 -
anonymous()
:表示可以匿名访问匹配的URL。 -
denyAll()
:表示所匹配的URL都不允许被访问。 -
authenticated()
:表示所匹配的URL都需要被认证才能访问 -
rememberMe()
:允许通过remember-me登录的用户访问 -
hasRole()
:如果有参数,参数表示角色,则其角色可以访问 - WWW-Authenticate: 表示服务器告知浏览器进行代理认证工作。
- Basic: 表示认证类型为Basic认证。
- realm=”Realm”: 表示认证域名为Realm域。
- 如果认证错误,浏览器会保持弹框;
- 如果认证成功,浏览器会缓存有效的Base64编码,在之后的请求中,浏览器都会在请求头中添加该有效编码。
- /login(get) : get请求时会跳转到这个页面,只要我们访问任意一个需要认证的请求时,都会跳转到这个登录界面。
- /login(post) : post请求时会触发这个接口,在登录页面点击登录时,默认的登录页面表单中的action就是关联这个login接口。
- /login?error : 当用户名或密码错误时,会跳转到该页面。
- /: 登录成功后,默认跳转到该页面,如果配置了index.html页面,则 ”/“ 会重定向到index.html页面,当然这个页面要由我们自己实现。
- /logout: 注销页面。
- /login?logout: 注销成功后跳转到的页面。
-
myLogin.html
页面,注意form表单中action的值,请暂时先写成”/myLogin.html“。 - defaultSuccessUrl
- successForwardUrl
- failureUrl()
- failureForwardUrl()
-
successHandler()
:successHandler方法的参数是一个AuthenticationSuccessHandler
对象,这个对象中我们要实现的方法是onAuthenticationSuccess
。 -
onAuthenticationSuccess()
:方法中有三个参数,分别是:-
HttpServletRequest
: 利用该参数我们可以实现服务端的跳转; -
HttpServletResponse
: 利用该参数我们可以做客户端的跳转,也可以返回 JSON 数据; -
Authentication
: 这个参数则保存了我们刚刚登录成功的用户信息。 - 定义SecurityAuthenticationSuccessHandler类:该类需要实现SavedRequestAwareAuthenticationSuccessHandler接口。
- 配置类:在SecurityConfig配置类中,调用successHandler()方法,把前面定义的SecurityAuthenticationSuccessHandler类关联进来。
- 定义类SecurityAuthenticationFailureHandler,实现ExceptionMappingAuthenticationFailureHandler接口,来专门处理认证失败时的返回结果。
- 配置类:调用failureHandler()方法
-
定义一个SecurityLogoutSuccessHandler类,实现
LogoutSuccessHandler
接口,在这里负责输出退出登录时的JSON结果。 -
配置类:调用
logoutSuccessHandler()
-
定义一个SecurityAuthenticationEntryPoint类,实现
AuthenticationEntryPoint
接口,在这里负责输出未认证时的JSON结果。 -
配置类:调用
authenticationEntryPoint()
方法 - 支持基于 URL 的请求授权
- 基于方法访问的授权
- 基于对象访问的授权
- 基于内存模型实现授权
- 基于默认数据库模型实现授权
- 基于自定义数据库模型实现授权
- UserDetailsService接口的源码:
- UserDetails接口源码:
- /visitor/hello 任何人都可以访问;
- /admin/hello 具有 admin 角色的人才能访问;
- /user/hello 具有 user 角色的人才能访问;
- 所有 user 角色能够访问的接口资源,admin 角色也都能够访问。
- accountNonExpired、accountNonLocked、credentialsNonExpired、enabled 这四个属性分别用来描述用户的状态,表示账户是否没有过期、账户是否没有被锁定、密码是否没有过期、以及账户是否可用;
- roles 属性表示用户的角色;
- getAuthorities 方法返回用户的角色信息,一个用户可能会有多个角色,所以这里返回值是一个集合类型,我们在这个方法中把自己的 Role 角色稍微转化一下即可。
- 基于散列加密算法机制 :加密用户必要的登录信息,并生成令牌来实现自动登录,利用 TokenBasedRememberMeServices 类来实现。
- 基于数据库等持久化数据存储机制 :生成持久化令牌来实现自动登录,利用 PersistentTokenBasedRememberMeServices 来实现。
- 首先后端根据登陆成功的用户的用户名,密码和过期时间等信息散列生成cookie,发回并保存在浏览器中;
-
在浏览器关闭并重新打开之后,用户再去访问
/user/hello
接口时,此时浏览器就会携带remember-me这个cookie到服务端; - 服务器端拿到cookie之后,利用Base64进行解码,计算出用户名和过期时间,再根据用户名查询到用户密码;
- 最后还要通过 MD5 散列函数计算出散列值,并将计算出的散列值和浏览器传递来的散列值进行对比,以此确认这个令牌是否有效。
- 首先从登录成功的 Authentication 对象中提取出用户名/密码;
- 由于登录成功之后,密码可能被擦除了,所以如果一开始没有拿到密码,就再从 UserDetailsService 中重新加载用户并重新获取密码;
- 接下来获取令牌的有效期,令牌有效期默认是两周;
- 再接下来调用 makeTokenSignature()方法 去计算散列值,实际上就是根据 username、令牌有效期以及 password、key 一起计算一个散列值。如果我们没有自己去设置这个 key,默认是在 RememberMeConfigurer#getKey 方法中进行设置的,它的值是一个 UUID 字符串。但是如果服务端重启,这个默认的 key 是会变的,这样就导致之前派发出去的所有 remember-me 自动登录令牌失效,所以我们可以指定这个 key。
- 最后,将用户名、令牌有效期以及计算得到的散列值放入 Cookie 中并随response返回。
- 自定义退出接口
- register接口
-
配置类:放行
/user/register
接口,注入PasswordEncoder
- 利用Ant表达式实现权限控制;(已学)
- 利用授权注解结合SpEl表达式实现权限控制;
- 利用过滤器注解实现权限控制;
- 利用动态权限实现权限控制。
-
@PreAuthorize
:方法执行前进行权限检查; -
@PostAuthorize
:方法执行后进行权限检查; -
@Secured
:类似于 @PreAuthorize。 - Java后端进行跨域解决CORS
- 使用AJAX的JSONP
- 使用jQuery的JSONP插件
- document.domain + iframe 跨域解决方案
- window.name + iframe 跨域解决方案
- location.hash + iframe 跨域解决方案
- postMessage跨域解决方案
- WebSocket协议跨域解决方案
- node代理跨域解决方案
- nginx代理跨域解决方案
- 方式1:在接口方法上利用@CrossOrigin注解解决跨域问题
- 方式2:通过实现WebMvcConfigurer接口来解决跨域问题
- MODE_THREADLOCAL :表示将 SecurityContext对象 存储在当前线程中,(默认);
- MODE_INHERITABLETHREADLOCAL :表示将 SecurityContext对象 存储在线程中,但子线程可以获取到父线程中的 SecurityContext;
- MODE_GLOBAL :表示 SecurityContext对象内容 在所有线程中都相同。
-
addFilterAt(filter,atFilter)
:这个相当于线性表的插入操作,把 filter 插入在 atFilter 的位置上。 -
addFilterBefore(filter,atFilter)
:这个是在 atFilter 前插入一个 filter 过滤器; -
addFilterAfter(filter,atFilter)
:这个顾名思义是在 atFilter 后插入一个 filter 过滤器; -
在ProviderManager类中遍历List\
providers集合,判断AuthenticationProvider类是否支持对当前对象进行认证; - 如果支持,则调用AuthenticationProvider接口对象的authenticate(authentication)方法进行认证;
- 结合当前具体的认证实现模型,如果是基于数据库模型来进行表单认证,则执行AbstractUserDetailsAuthenticationProvider类中的authenticate(authentication)方法具体进行认证;
- AbstractUserDetailsAuthenticationProvider类中的authenticate(authentication)方法内部会调用retrieveUser()抽象方法加载对象;
- 在DaoAuthenticationProvider类中具体执行retrieveUser()方法,在该方法中调用UserDetailsService对象的loadUserByUsername(username)方法,从而实现根据用户名从数据库中查询用户信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
* 处理登录成功时的业务逻辑
*/
public class SecurityAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
/**
* Authentication:携带登录的用户名及角色等信息
*/
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
//直接输出json格式的响应信息
Object principal = authentication.getPrincipal();
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
//以json格式对外输出身份信息
out.write(new ObjectMapper().writeValueAsString(principal));
out.flush();
out.close();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
// 认证成功时的处理器
.successHandler(new SecurityAuthenticationSuccessHandler())
.and()
.csrf()
.disable();
}
}登录成功后,后端向前端发送json:
2.4.3 认证失败
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 处理登录失败时的业务逻辑
*/
public class SecurityAuthenticationFailureHandler extends ExceptionMappingAuthenticationFailureHandler {
/**
* AuthenticationException:异常信息
*/
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//直接输出json格式的响应信息
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write(exception.getMessage());
out.flush();
out.close();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
// 认证成功时的处理器
.successHandler(new SecurityAuthenticationSuccessHandler())
// 认证失败时的处理器
.failureHandler(new SecurityAuthenticationFailureHandler())
.and()
.csrf()
.disable();
}
}登录失败:
2.4.4 退出登录
1
2
3
4
5
6
7
8
9
10public class SecurityLogoutSuccessHandler implements LogoutSuccessHandler {
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("注销成功");
out.flush();
out.close();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
// 认证成功时的处理器
.successHandler(new SecurityAuthenticationSuccessHandler())
// 认证失败时的处理器
.failureHandler(new SecurityAuthenticationFailureHandler())
.and()
.logout()
//退出登录
.logoutSuccessHandler(new SecurityLogoutSuccessHandler())
.and()
.csrf()
.disable();
}2.4.5 未认证
1
2
3
4
5
6
7
8
9
10public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("尚未登录,请先登录");
out.flush();
out.close();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
// 认证成功时的处理器
.successHandler(new SecurityAuthenticationSuccessHandler())
// 认证失败时的处理器
.failureHandler(new SecurityAuthenticationFailureHandler())
.and()
.logout()
.logoutSuccessHandler(new SecurityLogoutSuccessHandler())
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(new SecurityAuthenticationEntryPoint());
}3 授权
所谓授权,比如说某个用户想要访问某个资源(接口、页面、功能等),我们应该先去检查该用户是否具备对应的权限,如果具备就允许访问,如果不具备,则不允许访问。
在Spring Security中,授权粒度有如下几种:
授权实现方式:
3.1 基于内存模型
3.2 基于默认数据库模型
3.3 基于自定义数据库模型
3.3.1 接口
Spring Security 支持MySQL、Oracle等多种不同的数据源,这些不同的数据源最终都由
UserDetailsService
这个接口的子类来负责进行操作。Spring Security还提供了另一个
UserDetailsService
的实现子类,也就是JdbcUserDetailsManager
。利用JdbcUserDetailsManager可以帮助我们以JDBC的方式对接数据库进行增删改查等操作。它内部设定了一个默认的数据库模型,只要遵从这个模型,我们就可以很方便的实现在数据库中创建用户名和密码、角色等信息,但是灵活性不足。1
2
3public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}从源码中可以看出,可以利用loadUserByUsername()方法,根据用户名查询出对应的UserDetails信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}从UserDetails的源码中我们了解到,UserDetails其实就是一个包含了User信息的类,其中包含了用户名、密码、角色及账号状态等信息。
3.3.2 代码实现
① 测试接口
1
2
3
4
5
6
7
8
public class AdminController {
public String hello() {
return "hello, admin";
}
}1
2
3
4
5
6
7
8
public class UserController {
public String hello() {
return "hello, user";
}
}1
2
3
4
5
6
7
8
public class VisitorController {
public String hello() {
return "hello, visitor";
}
}授权规则:
② 准备数据库
1
2
3
4
5
6
7
8CREATE TABLE users (id bigint(20) NOT NULL AUTO_INCREMENT,
username varchar(50) NOT NULL,
password varchar(60) NOT NULL,
enable tinyint(4) NOT NULL DEFAULT 1,
roles text character set utf8,
PRIMARY KEY (id),
KEY username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;③ User实体类
创建一个User实体类,这个用户实体类需要实现 UserDetails 接口,并实现接口中的方法。
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
public class User implements UserDetails {
private Long id;
private String username;
private String password;
private String roles;
private boolean enable;
private List<GrantedAuthority> authorities;
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
public String getPassword() {
return this.password;
}
public String getUsername() {
return this.username;
}
public boolean isAccountNonExpired() {
return true;
}
public boolean isAccountNonLocked() {
return true;
}
public boolean isCredentialsNonExpired() {
return true;
}
public boolean isEnabled() {
return true;
}
}④ Mapper接口
1
2
3
4
5
public interface UserMapper {
User findByUserName( String username);
}⑤ 实现UserDetailsService接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyUserDetailsService implements UserDetailsService {
private UserMapper userMapper;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 从数据库尝试读取该用户
User user = userMapper.findByUserName(username);
// 用户不存在,抛出异常
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 将数据库形式的roles解析为UserDetails的权限集
// AuthorityUtils.commaSeparatedStringToAuthorityList是Spring Security
// 提供的用于将逗号隔开的权限集字符串切割成可用权限对象列表的方法
// 当然也可以自己实现,如用分号来隔开等,参考generateAuthorities
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
return 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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private MyUserDetailsService userDetailsService;
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 访问/admin/**需要ADMIN角色
.antMatchers("/admin/**")
.hasRole("ADMIN")
// 访问/user/**需要USER角色
.antMatchers("/user/**")
.hasRole("USER")
// 访问/visitor/**准予放行
.antMatchers("/visitor/**")
.permitAll()
// 其他请求需要授权
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//关联UserDetailsService对象
auth.userDetailsService(userDetailsService)
//暂时不对密码进行加密配置
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}4 验证码
5 自动登录和注销
5.1 实现方案
自动登录是将用户的登录信息保存在用户浏览器的cookie中,当用户下次访问时,自动实现校验并建立登录状态的一种机制 。
所以基于上面的原理,Spring Security 就为我们提供了两种比较好的实现自动登录的方案:
5.2 基于散列加密算法
5.2.1 代码实现
① 配置加密令牌key
创建一个application.yml文件,在其中添加数据库配置,以及 一个用来加密令牌的key字符串,字符串的值随便自定义就行 。
1
2
3
4
5
6
7
8spring:
datasource:
url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
username: XXX
password: XXX
security:
remember-me:
key: zym② 配置类
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 注入key
private String rememberKey;
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.antMatchers("/visitor/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
// 认证成功时的处理器
.successHandler(new SecurityAuthenticationSuccessHandler())
// 认证失败时的处理器
.failureHandler(new SecurityAuthenticationFailureHandler())
.and()
// 开启“记住我”
.rememberMe()
.userDetailsService(userDetailsService)
.key(rememberKey)
.and()
.csrf()
.disable();
}
private MyUserDetailsService userDetailsService;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//关联UserDetailsService对象
auth.userDetailsService(userDetailsService)
//暂时不对密码进行加密配置
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}请求结果:
5.2.2 cookies加密原理
1
2// 对各字段进行散列加密
hashInfo=md5Hex(username +":"+expirationTime +":"password+":"+key)将该
hashInfo
作为cookies存储在浏览器。再次登录时:
1
2//利用base64进行解码
rememberCookie=base64(username+":"+expirationrime+":"+hashInfo)5.2.3 自动登录原理
① 令牌生成
令牌生成的核心处理方法定义在:
TokenBasedRememberMeServices#onLoginSuccess
。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
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
//从认证对象中获取用户名
String username = retrieveUserName(successfulAuthentication);
//从认证对象中获取密码
String password = retrievePassword(successfulAuthentication);
......
if (!StringUtils.hasLength(password)) {
//根据用户名查询出对应的用户
UserDetails user = getUserDetailsService().loadUserByUsername(username);
//获取到用户身上的密码
password = user.getPassword();
}
//获取登录过期时间,默认是2周
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
//生成remember-me签名信息
String signatureValue = makeTokenSignature(expiryTime, username, password);
//保存cookie
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
tokenLifetime, request, response);
}
protected String makeTokenSignature(long tokenExpiryTime, String username,
String password) {
String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
MessageDigest digest;
digest = MessageDigest.getInstance("MD5");
return new String(Hex.encode(digest.digest(data.getBytes())));
}② 令牌解析
5.3 基于持久化令牌
持久化令牌方案 避免了散列加密方案中,一个令牌可以同时在多端登录的问题 ,这是因为 每个session会话都会引发token的更新,即每个token仅支持单实例登录 。
5.3.1 代码实现
① 数据表
创建一张persistent_logins表,用来存储我们自动登录时生成的持久化令牌信息,该表SQL脚本如下:
1
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)
在该表中,series是主键,可以根据series进行令牌信息的查询等操作。
② 配置类
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private String rememberKey;
private DataSource dataSource;
protected void configure(HttpSecurity http) throws Exception {
//配置数据源
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.antMatchers("/visitor/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
// 开启“记住我”
.rememberMe()
.userDetailsService(userDetailsService)
.key(rememberKey)
// 持久化令牌方案
.tokenRepository(tokenRepository)
// 设置令牌有效期,为7天有效期
.tokenValiditySeconds(60 * 60 * 24 * 7)
.and()
.csrf()
.disable();
}
private MyUserDetailsService userDetailsService;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//关联UserDetailsService对象
auth.userDetailsService(userDetailsService)
//暂时不对密码进行加密配置
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}这时候,我们只需要在登录页面中输入 用户名和密码,勾选“记住我”功能之后,Spring Security就会生成一个持久化令牌,在这个令牌中就保存了当前登陆的用户信息,该令牌信息会被自动持久化存储到persistent_logins表中。
5.3.2 两种方式的对比
如果我们一定要实现自动登录功能, 可以限制以cookie身份登录时的部分执行权限** ,比如在修改密码、修改邮箱(防止找回密码)、查看隐私信息(如完整的手机号码、银行卡号等)时,我们可以进一步校验用户的登录密码,或者设置独立密码来做二次校验,以提高安全性。
5.4 注销
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
38http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.hasRole("USER")
.antMatchers("/app/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
//开启记住我功能
.rememberMe()
.userDetailsService(userDetailsService)
//1.散列加密方案
.key(rememberKey)
//2.持久化令牌方案
.tokenRepository(tokenRepository)
//7天有效期
.tokenValiditySeconds(60 * 60 * 24 * 7)
.and()
//配置退出登录功能
.logout()
//关联自己的退出登录接口
.logoutUrl("/user/logout")
//注销成功,重定向到该路径下
.logoutSuccessUrl("/login")
//使得session失效
.invalidateHttpSession(true)
//清除认证信息
.clearAuthentication(true)
//删除指定的cookie
.deleteCookies("cookie01","cookie02")
.and()
.csrf()
.disable();如果我们想自己编写退出登录时的业务逻辑,也可以在UserController中定义一个“/logout”接口,处理退出登录功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class UserController {
public String hello() {
return "hello, user";
}
public void logout(HttpSession session){
session.invalidate();
System.out.println("logout执行了...");
}
}6 密码加密
6.1 简介
Spring Security提供了多种密码加密算法,但官方推荐使用的是 BCryptPasswordEncoder 方案。
6.2 代码实现
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
public class UserController {
private PasswordEncoder passwordEncoder;
private UserService userService;
public String hello() {
return "hello, user";
}
public User registerUser( User user){
user.setEnable(true);
user.setRoles("USER");
//对密码进行加密
user.setPassword(passwordEncoder.encode(user.getPassword()));
userService.save(user);
return 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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private String rememberKey;
private DataSource dataSource;
protected void configure(HttpSecurity http) throws Exception {
//配置数据源
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
// 放行register接口
.antMatchers("/user/register")
.permitAll()
.antMatchers("/user/**")
.hasRole("USER")
.antMatchers("/visitor/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
// 开启“记住我”
.rememberMe()
.userDetailsService(userDetailsService)
.key(rememberKey)
// 持久化令牌方案
.tokenRepository(tokenRepository)
// 设置令牌有效期,为7天有效期
.tokenValiditySeconds(60 * 60 * 24 * 7)
.and()
.csrf()
.disable();
}
private MyUserDetailsService userDetailsService;
public PasswordEncoder passwordEncoder() {
//使用默认的BCryptPasswordEncoder加密方案
return new BCryptPasswordEncoder();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//关联UserDetailsService对象
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}测试接口:
7 权限控制
在Spring Security 中,对接口的拦截或放行,有四种常见的权限控制方式:
7.1 授权注解结合SpEl表达式
首先需要 利用@EnableGlobalMethodSecurity注解开启授权注解功能 ,代码如下:
1
2
3
4
5
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// ...
// ...
}然后在具体的接口方法上利用授权注解进行权限控制,代码如下:
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
public class UserController {
//@PreAuthorize("principal.username.equals('user')")
public String helloUser() {
return "hello, user";
}
public String helloAdmin() {
return "hello, admin";
}
public String getAge( Integer age){
return String.valueOf(age);
}
public String helloVisitor() {
return "hello, visitor";
}
}7.2 过滤器注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FilterController {
/**
* 只返回结果中id为偶数的user元素。
* filterObject是@PreFilter和@PostFilter中的一个内置表达式,表示集合中的当前对象。
*/
public List<User> getAllUser() {
List<User> users = new ArrayList<>();
for (int i = 0; i < 10; i++) {
users.add(new User(i, "zym-" + i));
}
return users;
}
}8 跨域问题
8.1 简介
在前后端分离的项目中,需要解决跨域问题,常用的手段:
8.2 代码实现
8.2.1 未引入ss的跨域解决方案
1
2
3
4
5
6
7
8
9
10
11
12
13
public class IndexController {
public String hello() {
return "get hello";
}
public String hello2() {
return "post hello";
}
}定义一个
index.html
页面,利用axios跨域访问8080项目中的web接口。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
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.26.0/axios.js"></script>
<link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/4.6.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- <h2 class="page-header">基本使用</h2> -->
<button class="btn btn-primary">发送GET请求</button>
<button class="btn btn-warning">发送POST请求</button>
</div>
<script>
// 获取按钮
const btns = document.querySelectorAll("button");
// 发送GET请求
btns[0].onclick = function() {
axios({
// 请求类型
method: "GET",
// 请求URL
url: "http://localhost:8080/hello"
}).then(response => {
console.log(response);
});
};
// 发送POST请求
btns[1].onclick = function() {
axios({
// 请求类型
method: "POST",
// 请求URL
url: "http://localhost:8080/hello"
}).then(response => {
console.log(response);
});
};
</script>
</body>
</html>然后分别执行get与post请求,这时候就可以在浏览器的控制台上看到产生了CORS跨域问题,出现了
CORS error
状态,在请求头中出现了Referer Policy: strict-origin-when-cross-origin
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IndexController {
public String hello() {
return "get hello";
}
public String hello2() {
return "post hello";
}
}1
2
3
4
5
6
7
8
9
10
11
12
public class WebMvcConfig implements WebMvcConfigurer {
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://127.0.0.1:5500/")
.allowedMethods("*")
.allowedHeaders("*");
}
}然后成功请求:
8.2.2 引入ss的跨域解决方案
为了提高网站的安全性,在上面Spring Boot项目的基础之上,添加Spring Security的依赖包,但是暂时不进行任何别的操作。
接着重启8080这个Spring Boot项目,然后在前端中再次进行跨域请求,我们会发现在引入Spring Security后,再次产生了跨域问题。
通过实验可知,如果使用了 Spring Security,上面的跨域配置会失效,因为请求会被 Spring Security 拦截。那么在Spring Security环境中,如何解决跨域问题呢?这里我们有3种方式可以开启 Spring Security 对跨域的支持。
① 开启cors方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.formLogin()
.permitAll()
.and()
.httpBasic()
.and()
//支持跨域访问
.cors()
.and()
.csrf()
.disable();
}
}② 进行全局配置
第二种方式是去除上面的跨域配置,直接在 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
25
26
27
28
29
30
31
32
33
34
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.permitAll()
.and()
.formLogin()
.permitAll()
.and()
.httpBasic()
.and()
.cors()
.configurationSource(corsConfigurationSource())
.and()
.csrf()
.disable();
}
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true);
configuration.addAllowedOriginPattern("*");
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(Duration.ofHours(1));
source.registerCorsConfiguration("/**", configuration);
return source;
}
}9 源码分析
9.1 整体流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16sequenceDiagram
participant UPAF as UsernamePassword<br/>AuthenticationFilter
participant AM as Authentication<br/>Manager
participant DAP as DaoAuthentication<br/>Provider
participant UDS as UserDetails<br/>Service
participant SCH as SecurityContext<br/>Holder
UPAF->>UPAF: 1.将请求信息封装为Authentication,<br/>实现类为UsernamePasswordAuthenticationToken
UPAF->>AM: 2.认证authenticate()
AM->>DAP: 3.委托认证authenticate()
DAP->>UDS: 4.获取用户信息<br/>loadUserByUsername()
UDS->>DAP: 5.返回UserDetails
DAP->>DAP: 6.通过passwordEncoder对比UserDetails中的密码与Authentication<br/>中的密码一致
DAP->>DAP: 7.填充Authentication,如权限信息
DAP->>UPAF: 8.返回Authentication
UPAF->>SCH: 9.通过SecurityContextHolder.getContext().setAuthentication()方法将Authentication保存至安全上下文9.2 基础API
Authentication,SecurityContext,SecurityContextHolder。三者关系:
SecurityContextHolder 用来保存 SecurityContext,利用SecurityContextHolder的getContext()方法可以得到SecurityContext对象;然后再通过调用 SecurityContext 对象中的getAuthentication()方法,就可以获取 Authentication 对象;最后我们利用Authentication对象,可以进一步获取已认证用户的详细信息。
9.2.1 Authentication接口
1
2
3
4
5
6
7
8
9
10
11
12
13public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}作用:在用户登录认证之前, 用户名密码等信息 会被封装为一个 Authentication 的具体实现类对象。在登录认证成功之后又会生成一个信息更全面的Authentication对象,从该对象中可 以得到用户的权限信息列表、密码、用户细节信息、用户身份信息、认证信息等 。这个 Authentication对象会被保存在 SecurityContextHolder 所持有的 SecurityContext 中 ,供后续的程序进行调用,如访问权限的鉴定等。
9.2.2 SecurityContext接口
1
2
3
4
5public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}9.2.3 SecurityContextHolder类
① 线程安全性
在Web环境下,SecurityContextHolder是利用 ThreadLocal 来存储SecurityContext对象的。所以SecurityContextHolder可以用来设置和获取SecurityContext对象,该类主要是给框架内部使用,我们可以利用它获取当前用户的SecurityContext对象,进而进行请求检查,和访问控制等。
因为Sevlet中的线程都是被池化复用的,一旦处理完当前的请求,这个线程可能马上就会被分配去处理其他的请求,而且也不能保证用户下次的请求会被分配到与上次相同的线程。因此在每一次请求结束后都会自动清除当前线程的ThreadLocal对象。
② 存储策略
SecurityContextHolder可以通过设置来调整3种存储策略,三种策略详情如下:
9.3 Filter API
9.3.1 内置过滤器
9.3.2 自定义过滤器
添加过滤器的方法(都是HttpSecurity中的),第一个参数都是要加入的过滤器,第二个参数是针对已存在过滤器的Class对象:
可以继承Spring Security中内置的过滤器以进行重写逻辑,另一方面可以继承Spring框架的过滤器。
例如:
OncePerRequestFilter
是Spring框架提供的一个过滤器,确保在一次HTTP请求期间只执行一次特定的过滤器逻辑。它继承了GenericFilterBean类,并实现了javax.servlet.Filter接口。1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//执行你的过滤逻辑
System.out.println("执行过滤逻辑");
// 继续处理请求
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private MyFilter myFilter;
// ...
protected void configure(HttpSecurity httpSecurity) throws Exception {
// httpSecurity的配置...
// ...
// 在处理表单认证之前,添加自定义过滤器
httpSecurity.addFilterBefore(myFilter, UsernamePasswordAuthenticationFilter.class);
}
}9.4 认证管理API
9.4.1 AuthenticationManager接口
AuthenticationManager的作用是校验Authentication,如果验证失败会抛出AuthenticationException异常 。
1
2
3public interface AuthenticationManager {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
}9.4.2 ProviderManager类
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// org/springframework/security/authentication/ProviderManager.java
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
//维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
// 遍历providers列表,判断是否支持当前authentication对象的认证方式
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
// 执行provider的认证方式并获取返回结果
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
// 若当前ProviderManager无法完成认证操作,且其包含父级认证器,则允许转交给父级认证器尝试进行认证
if (result == null && parent != null) {
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// 完成认证,从authentication对象中移除私密数据
((CredentialsContainer) result).eraseCredentials();
}
// 若父级AuthenticationManager认证成功,则派发AuthenticationSuccessEvent事件
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// 未认证成功,抛出ProviderNotFoundException异常
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
}9.5 认证实现API
9.5.1 AuthenticationProvider接口
1
2//维护一个AuthenticationProvider列表
private List<AuthenticationProvider> providers = Collections.emptyList();AuthenticationProvider
接口有多个实现子类。9.5.2 DaoAuthenticationProvider类
DaoAuthenticationProvider 在进行认证的时候,需要调用 UserDetailsService 对象的loadUserByUsername() 方法来获取用户信息 UserDetails,其中包括用户名、密码和所拥有的权限等。
authenticate()
认证方法源码剖析在AuthenticationProvider接口中有个authenticate()方法,但是该方法并没有默认实现。
1
2
3
4
5public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication) throws AuthenticationException;
boolean supports(Class<?> authentication);
}这个方法是在AuthenticationProvider的子类
AbstractUserDetailsAuthenticationProvider
中实现的,实现源码如下: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// 抽象方法
protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException;
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
// 从认证对象中得到用户名
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
// 根据用户名获取缓存的用户对象
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
// 在缓存为空的情况下,调用retrieveUser()方法,根据用户名查询用户对象。
// 其中的retrieveUser()方法是个抽象方法,由子类DaoAuthenticationProvider来实现。
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return createSuccessAuthentication(principalToReturn, authentication, user);
}retrieveUser
抽象方法由DaoAuthenticationProvider来具体实现:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {
prepareTimingAttackProtection();
try {
//这里调用了我们自己编写的UserDetailsService对象中的loadUserByUsername()方法!
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}catch (InternalAuthenticationServiceException ex) {
throw ex;
}catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}9.5.3 authenticate流程
9.6 实体接口API
9.6.1 UserDetails接口
UserDetails 是 Spring Security 中的核心接口,它表示用户的详细信息,这个接口涵盖了一些必要的用户信息字段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public interface UserDetails extends Serializable {
//获取用户权限,本质上是用户的角色信息
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//判断账户是否未过期
boolean isAccountNonExpired();
//判断账户是否未锁定
boolean isAccountNonLocked();
//判断密码是否未过期
boolean isCredentialsNonExpired();
//判断账户是否可用
boolean isEnabled();
}可以实现这个接口,然后自定义其他的属性。
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15http.authorizeRequests()
// 允许登录页面匿名访问
.antMatchers("/showLogin", "/errPage")
.anonymous()
// 所有的静态资源允许匿名访问
.antMatchers(
"/css/**",
"/js/**",
"/images/**",
"/fonts/**",
"/favicon.ico")
.anonymous()
// 其他所有的请求都需要登录认证
.anyRequest()
.authenticated();权限控制方法
2.1.3 认证流程
当第一次调用接口时,查看响应头,可以看到
WWW-Authenticate
认证信息:1
WWW-Authenticate:Basic realm="Realm"
根据401和以上响应头信息,浏览器会弹出一个对话框,要求输入用户名/密码,Basic认证会将其拼接成 “用户名:密码” 格式,中间是一个冒号,并利用Base64编码成加密字符串
xxx
;然后在请求头中附加Authorization: Basic xxx
,发送给后台认证;后台需要利用Base64来进行解码xxx
,得到用户名和密码,再校验 用户名:密码 信息。2.1.4 认证的销毁
2.2 Form表单认证
2.2.1 默认配置
下图显示,
hello
请求被重定向到login
:在默认的表单认证配置中,自动配置了一些url和页面:
2.2.2 自定义配置
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 用来定义哪些请求需要忽略安全控制,哪些请求必须接受安全控制;还可以在合适的时候清除SecurityContext以避免内存泄漏,
* 同时也可以用来定义请求防火墙和请求拒绝处理器,另外我们开启Spring Security Debug模式也是这里配置的
*/
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/js/**", "/css/**", "/images/**");
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 对任意请求都进行认证
.anyRequest()
.authenticated()
.and()
// 开启表单认证
.formLogin()
// 加载自定义的登录页面地址
.loginPage("/myLogin.html")
.permitAll()
.and()
.csrf()
.disable();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<html lang="en">
<body>
<div class="login">
<h2>Access Form</h2>
<div class="login-top">
<h1>登录验证</h1>
<form action="/myLogin.html" method="post">
<input type="text" name="username" placeholder="username" />
<input type="password" name="password" placeholder="password" />
<div class="forgot">
<a href="#">忘记密码</a>
<input type="submit" value="登录" >
</div>
</form>
</div>
<div class="login-bottom">
<h3>新用户 <a href="#">注 册</a></h3>
</div>
</div>
</body>
</html>访问接口,被重定向到
/myLogin.html
:2.2.3 细化配置
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
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 用来定义哪些请求需要忽略安全控制,哪些请求必须接受安全控制;还可以在合适的时候清除SecurityContext以避免内存泄漏,
* 同时也可以用来定义请求防火墙和请求拒绝处理器,另外我们开启Spring Security Debug模式也是这里配置的
*/
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/js/**", "/css/**", "/images/**");
}
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
//用and来表示配置过滤器结束,以便进行下一个过滤器的创建和配置
.and()
//设置表单登录,创建UsernamePasswordAuthenticationFilter
.formLogin()
.loginPage("/myLogin.html")
.permitAll()
//指登录成功后,是否始终跳转到登录成功url。它默认为false
.defaultSuccessUrl("/index.html",true)
//post登录接口,登录验证由系统实现
.loginProcessingUrl("/login")
//用户密码错误跳转接口
.failureUrl("/error.html")
//要认证的用户参数名,默认username
.usernameParameter("zym")
//要认证的密码参数名,默认password
.passwordParameter("zym123")
.and()
//配置注销
.logout()
//注销接口
.logoutUrl("/logout")
//注销成功后跳转到的接口
.logoutSuccessUrl("/myLogin.html")
.permitAll()
//删除自定义的cookie
.deleteCookies("myCookie")
.and()
//注意:需禁用crsf防护功能,否则登录不成功
.csrf()
.disable();
}
}2.3 HTTP摘要认证
2.4 前后端分离方案
2.4.1 概述
在之前的章节讲解表单认证时,处理登录成功时,跳转到某个页面的API是如下两个方法:
处理登录失败时,跳转页面的API是如下两个方法:
上面的方法,无论是认证成功还是认证失败,都是在前后端不分离时的处理方案,直接从Java后端跳转到某个页面上。在前后端分离模式下,既然后端没有页面,页面都在前端,那就可以考虑使用JSON来进行信息交互了,我们把认证成功或认证失败的信息,以JSON的格式传递给前端,由前端来决定到底该往哪个页面跳转就好了。
2.4.2 认证成功
① API
-
1 |
<dependency> |
1 |
|
把项目启动起来后,在浏览器中对Web接口进行访问,会发现接口是无法直接访问的。在访问接口之前会自动跳转到
/login
地址,进入到一个登录界面。这是因为Spring Boot中”约定大约配置”的规则,只要添加了Spring Security的依赖包,就会自动开启安全限制,在访问Web接口之前会进行安全拦截。只有输入了用户名和密码,才能访问项目中的Web接口。
默认用户名为
user
,密码为随机
uuid
:
1 |
public class ReactiveUserDetailsServiceAutoConfiguration { |
密码生成机制:
1 |
public class SecurityProperties { |
1 |
spring: |
2 认证
认证: 所谓的认证,就是用来判断系统中是否存在某用户,并判断该用户的身份是否合法的过程,解决的其实是用户登录的问题。认证的存在,是为了保护系统中的隐私数据与资源,只有合法的用户才可以访问系统中的资源。
在Spring Security中,常见的认证方式可以分为HTTP层面和表单层面,常见的认证方式如下:
2.1 HTTP基本认证
2.1.1 概述
HTTP基本认证是一种无状态的认证方式,与表单认证相比,HTTP基本认证是一种基于HTTP层面的认证方式,无法携带Session信息,也就无法实现Remember-Me功能。另外,用户名和密码在传递时仅做了一次简单的Base64编码,几乎等同于以明文传输,极易被进行密码窃听和重放攻击。所以在实际开发中,很少会使用这种认证方式来进行安全校验。
2.1.2 代码实现
1 |
|
在SecurityConfig类上添加
@EnableWebSecurity
注解后,会自动被Spring发现并注册。
注:
http.authorizeRequests()
首先对url进行匹配,然后进行权限控制。支持链式调用。
url匹配规则
1 |
http.authorizeRequests() |