默认登录页面通过DefaultLoginPageGeneratingFilter#generateLoginPageHtml生成
默认登录页面通过DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码: <input type="password" name="password"/><br/>
<input type="submit" value="提交"/>
form>
body>
html>
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单提交
.loginPage("/login.html") //自定义登录页面
.loginProcessingUrl("/user/login") //登录访问路径,必须和表单提交接口一样
.defaultSuccessUrl("/admin/index") //认证成功之后跳转的路径
.and().authorizeRequests()//设置哪些路径可以直接访问,不需要认证
.antMatchers("/user/login","/login.html")
.permitAll() .anyRequest().authenticated() //需要认证
.and().csrf().disable(); //关闭csrf防护
}
测试
访问/admin/demo直接返回结果,不用认证
访问/admin/index跳转到自定义登录界面
常见问题:
原因是登录只接受Post请求 如下,通过successForwardUrl和failureForwardUrl设置登录成功和失败后的跳转页面
@Override
protected void configure(HttpSecurity http) throws Exception {
// 认证
http.formLogin() //表单登录
.loginPage("/login.html") //自定义登录
.loginProcessingUrl("/user/login")
.successForwardUrl("/main") //认证成功之后转发的路径,必须是Post请求
.failureForwardUrl("/toerror") //认证失败之后转发的路径,必须是Post请求
.and().authorizeRequests()
// 授权指定的请求,不用认证
.antMatchers("/user/login","/login.html","/error.html").permitAll()
.anyRequest().authenticated()//需要认证
.and().csrf().disable(); //关闭csrf防护
}
controller.java
@Controller
public class LoginController {
@RequestMapping("/main")
public String main() {
return "redirect:/main.html";
}
@RequestMapping("/toerror")
public String error() {
return "redirect:/error.html";
}
}
自定义用户名和密码参数名
当进行登录时会执行 UsernamePasswordAuthenticationFilter 过滤器
2.1 过滤器链模式与责任链模式的区别
过滤器链模式又称标准模式,这种模式主要使用不同标准来过滤一组对象。
过滤的过程便是一个层层筛选的过程,因此过滤器模式属于结构型设计模式的一种。
责任链模式顾名思义就是创建一个链条,经过这个链条处理的所有对象和数据分别进行依次加工,每个环节负责处理不同的业务,环节间彼此解耦,同时可以复用。
通信行业如移动会有很多营销活动,而这些营销活动的对象是有要求的,有的需要判断在网时长,有的需要有最低套餐要求等;
1.中国移动客户是目标角色;
2.它不同营销活动的要求是过滤器角色;
3.登录认证服务器需要经历层层过滤校验
过滤器链模式属于结构型设计模式
责任链模式属于行为型设计模式
结构型模式的目的是通过组合类或对象产生更大结构以适应更高层次的逻辑需求
从实现代码过滤上来看,过滤器模式更像是为了分组group by,而责任链模式是让多个对象都有可能接受请求,将这些对象连接成一条链,并且沿着这条链传递请求。
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring
security提供会话管理,认证通过后将身份信息放入Security
ContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
这里做一个小复习,我们知道客户端请求服务器端是用http(超文本传输协议)请求,而http请求是无状态的,所以我们的状态是存在服务器(session)或者浏览器(cookie)
1.cookie数据存放在客户的浏览器上,session数据放在服务区上
2.cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗考虑到安全应当使用session。
3.设置cookie时间可以使cookie过期。但是使用session-destory(),我们将会销毁会话。
4.session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能考虑到减轻服务器性能方面,应当使用cookie。
5.单个cookie保存到数据不能超过4k,很多浏览器都限制一个站点最多保存20个cookie。(session对象没有对存储的数据量的限制,其中可以保存更为复杂的数据类型)
1、session很容易失效,用户体验很差;
2、虽然cookie不安全,但是可以加密 ;
3、cookie也分为永久和暂时存在的;
4、浏览器 有禁止cookie功能 ,但一般用户都不会设置;
5、一定要设置失效时间,要不然浏览器关闭就消失了;
@RestController
@RequestMapping("/admin")
public class AdminController {
@GetMapping("/demo")
public String demo() {
String username = getUsername();
return username + "spring security demo";
}
private String getUsername() {
//获取当前登录的用户信息
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (!authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
String username = null;
if(principal instanceof UserDetails) {
username = ((UserDetails)principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
}
机制 | 描述 |
---|---|
always | 如果session不存在总是需要创建 |
ifRequired | 如果需要就创建一个session(默认)登录时 |
never | Spring Security 将不会创建session,但是如果应用中其他地方创建了session,那 么Spring Security将会使用它 |
stateless | Spring Security将绝对不会创建session,也不使用session。并且它会暗示不使用 cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。 |
http.sessionManagement()
.invalidSessionUrl("/session/invalid");
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired 。在执行认 证过程之前,spring security将运行SecurityContextPersistenceFilter过滤器负责存储安全请求上下 文,上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository ,其使用http session 作为存储器。
可以在sevlet容器中设置Session的超时时间,如下设置Session有效期为600s; spring boot 配置文件:
注意:session最低60s,参考源码TomcatServletWebServerFactory#configureSession:
session超时之后,可以通过Spring Security 设置跳转的路径。
/**
* @author shengwencheng
* @sice 2021-11-05 10:36
*/
@RestController
@RequestMapping("/session")
public class SessionController {
@GetMapping("/invalid")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public String sessionInvalid() {
return "session失效";
}
}
用户在这个手机登录后,他又在另一个手机登录相同账户,对于之前登录的账户 是否需要被挤兑,或者说在第二次登录时限制它登录,更或者像腾讯视频VIP账号一样,最多只能五个人同时登录,第六个人限制登录。
http.sessionManagement()
.invalidSessionUrl("/session/invalid")
.maximumSessions(1)
.expiredSessionStrategy(new MyExpiredSessionStrategy());
public class MyExpiredSessionStrategy implements
SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event)
throws IOException, ServletException {
} }
HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("您已被挤兑下线!");
测试
阻止用户第二次登录
sessionManagement也可以配置 maxSessionsPreventsLogin:boolean值,当达到 maximumSessions设置的最大会话个数时阻止登录。
http.sessionManagement()
.invalidSessionUrl("/session/invalid")
.maximumSessions(1)
.expiredSessionStrategy(new MyExpiredSessionStrategy())
.maxSessionsPreventsLogin(true);
实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访 问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服 务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一次访问被 分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用户在服务器 一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉不正常 了。解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中,而是保存到一个单独的库 (redis、mongodb、mysql等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信 息,如用户在服务器一上登录,将会话信息保存到库中,用户的下次请求被分配到服务器二,服务器二 从库中检查session是否已经存在,如果存在就不用再登录了,可以直接访问服务了。
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
修改application.yaml
spring:
session:
store-type: redis
redis:
host: localhost
port: 6379
server:
port: 8080
servlet:
session:
timeout: 600
测试
启动两个服务8080,8081 ,其中一个登录后访问http://localhost:8080/admin/index,另外一个不需
要登录就可以访问
安全会话cookie 我们可以使用httpOnly和secure标签来保护我们的会话cookie:
spring boot配置文件:
server.servlet.session.cookie.http-only=true server.servlet.session.cookie.secure=true
Spring Security 中 Remember Me 为“记住我”功能,用户只需要在登录时添加 remember-me复选框,
取值为true。Spring Security 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
@Autowired
public DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
//记住我
http.rememberMe()
.tokenRepository(persistentTokenRepository())//设置持久化仓库
.tokenValiditySeconds(3600) //超时时间,单位s 默认两周
.userDetailsService(userServiceImpl); //设置自定义登录逻辑
}
public PersistentTokenRepository persistentTokenRepository(){ JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); //设置数据源
jdbcTokenRepository.setDataSource(dataSource);
return jdbcTokenRepository;
}
create table persistent_logins (
username varchar(64) not null,
series varchar(64) primary key,
token varchar(64) not null,
last_used timestamp not null
)
在客户端登录页面中添加 remember-me 的复选框,只要用户勾选了复选框下次就不需要进行登录了。
<form action="/user/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码: <input type="password" name="password"/><br/>
<input type="checkbox" name="remember-me" value="true"/><br/>
<input type="submit" value="提交"/>
form>
Spring security默认实现了logout退出,用户只需要向 Spring Security 项目中发送 /logout 退出请求即 可。
默认的退出 url 为 /logout ,退出成功后跳转到 /login?logout 。
自定义退出逻辑
如果不希望使用默认值,可以通过下面的方法进行修改。
http.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login.html");
SecurityContextLogoutHandler
当退出操作出发时,将发生:
销毁HTTPSession 对象 清除认证状态
跳转到 /login.html
LogoutSuccessHandler
退出成功处理器,实现 LogoutSuccessHandler 接口 ,可以自定义退出成功处理逻辑。
1、CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者Session Riding。 通过伪造用户请求访问受信任站点的非法请求访问。
2、跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求。
3、客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意劫 持,通过这个session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不 到的事情。
防御CSRF攻击策略
从 Spring Security4开始CSRF防护默认开启,默认会拦截请求,进行CSRF处理。CSRF为了保证不是其 他第三方网站访问,要求访问时携带参数名为 _csrf 值为token(token 在服务端产生,在渲染请求页面时 埋入页面)的内容,如果token和服务端的token匹配成功,则正常访问。
修改login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/login" method="post">
<input type="hidden" th:value="${_csrf.token}" name="_csrf"
th:if="${_csrf}"/>
用户名:<input type="text" name="username"/><br/> 密码: <input type="password" name="password"/><br/> <input type="submit" value="提交"/>
</form>
</body>
</html>
修改配置类
//关闭csrf防护
// http.csrf().disable();
以上就是Spring Security的认证使用,下篇是认证服务器整体流程的梳理,喜欢的朋友点个关注,顺便推荐一个不错的Spring Cloud Alibaba + vue的项目,https://gitee.com/youlaitech/youlai-mall
-> SpringSecurity原理剖析及其实战(四)