一个简洁的博客网站:http://lss-coding.top,欢迎大家来访
学习娱乐导航页:http://miss123.top/
什么是安全框架?解决系统安全问题的框架,如果没有安全框架。我们需要手动处理每个资源的访问控制,非常麻烦,使用安全框架,我们可以通过配置的方式实现对资源的访问控制。
Spring Security,Spring 家族的成员,是一个能够为基于 Spring 的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在 Spring 应用上下文中配置 Bean。充分利用 Spring IOC、DI 和 AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Apach Shiro,功能强大且易于使用的 Java 安全框架,提供了认证、授权、加密和会话管理。
一个高度自定义的安全框架。利用 IOC/DI和AOP 功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Security 的原因有很多,但大部分都是发现了 JavaEE 和 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新匹配你的应用程序。使用 Spring Security 解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证”和“授权”或者称为访问控制。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他生命的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),就是说系统认为用户是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。就是系统判断用户是否有权限去做某一些事情。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<form action="/login" method="post">
<input type="text" name="username" /><br>
<input type="password" name="password"><br>
<button type="submit">登录button>
form>
body>
html>
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>登录成功h1>
body>
html>
@Controller
public class LoginController {
/**
* @description 登录
* @author lishisen
* @date 2021/12/27 8:15
**/
@RequestMapping("/login")
public String login() {
return "redirect:main.html";
}
}
启动测试
启动后访问登录请求会发现,首先进入的是一个登录页面,这个登录页面是 Spring Security 内置的一个登录页面。这个登录账号默认是 user,密码在控制台有输出(每一次都不一样)。当这个判断我们的用户名和密码正确的时候才进入到我们自定义的登录页面。
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现,是基于 Hash 算法实现的单向加密,可以通过 strength 控制加密强度,默认 10。
@Test
void contextLoads() {
// 创建一个密码解析器
BCryptPasswordEncoder pw = new BCryptPasswordEncoder();
// 对密码进行加密,相同的值每一次加密都是会不同的
String encode = pw.encode("123456");
System.out.println(encode);
// 判断用户给定的明文是否与解密后的密文相等,相等返回true,否则返回false
boolean matches = pw.matches("12345", encode);
System.out.println(matches);
}
@Configuration
public class SpringSecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private BCryptPasswordEncoder pw;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1. 根据用户名去数据库查询,如果不存在则抛出 UsernameNotFoundException 异常
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户不存在");
}
// 2. 比较密码(注册时已经加密过),如果匹配成功返回 UserDetails
String password = pw.encode("123");
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
启动测试
启动后我们可以发现,控制台不会在打印一个 SpringSecurity 默认的密码
登录的时候使用自定义的用户:admin、密码:123 就可以进行登录了。
虽然 Spring Security 提供了登录页面,但是实际项目中不会用到的,大多数情况使用自己的登录页面。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 登录成功后跳转的页面,POST 请求
.successForwardUrl("/toMain");
// 授权
http.authorizeRequests()
// 放行 /login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
// 关闭 csrf 防护
http.csrf().disable();
}
/**
* @description 登录到主页
* @author lishisen
* @date 2021/12/27 8:15
**/
@RequestMapping("/toMain")
public String login() {
return "redirect:main.html";
}
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
登录失败,点击 <a href="/login.html">跳转a>到登录页面
body>
html>
/**
* @description 跳转到错误页面
* @author lishisen
* @date 2021/12/27 9:35
**/
@RequestMapping("/toError")
public String toError() {
return "redirect:error.html";
}
// 表单提交
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 登录成功后跳转的页面,POST 请求
.successForwardUrl("/toMain")
// 登录失败后跳转的页面
.failureForwardUrl("/toError");
// 授权
http.authorizeRequests()
// 放行 /error.html,不需要认证
.antMatchers("/error.html").permitAll()
// 放行 /login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
<form action="/login" method="post">// 必须为 post 请求
用户名:<input type="text" name="username" /><br>// name 必须为 username
密码: <input type="password" name="password"><br>// name 必须为 password
<button type="submit">登录button>
form>
以上的约束在这个类中都进行了定义:UsernamePasswordAuthenticationFilter
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login",
"POST");
......
private boolean postOnly = true;f
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
// 如果不是 post 请求就会抛出异常
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
username = (username != null) ? username : "";
username = username.trim();
String password = obtainPassword(request);
password = (password != null) ? password : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
// 使用 request.getParameter(); 获取用户名或者密码
@Nullable
protected String obtainPassword(HttpServletRequest request) {
// passwordParameter==passowrd
return request.getParameter(this.passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
// usernameParameter==username
return request.getParameter(this.usernameParameter);
}
http.formLogin();
链上追加http.formLogin()
// 自定义的 username123
.usernameParameter("username123")
// 自定义的 password123
.passwordParameter("password123")
有一个需求:登录成功跳转到 http://wwwbaidu.com 这个网站,修改 http.formLogin() 中的 successForwardUrl
// 表单提交
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 登录成功后跳转的页面,POST 请求
// 对这里进行修改
.successForwardUrl("http://www.baidu.com")
// 登录失败后跳转的页面
.failureForwardUrl("/toError");
修改后经过测试结果为 404
查看源码发现,底层其实就是一个简单的页面转发,转发是不能跳转到百度页面的,所以我们下面自定义可以使用重定向
public class ForwardAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
request.getRequestDispatcher(this.forwardUrl).forward(request, response);
}
public interface AuthenticationSuccessHandler {
/**
* Called when a user has been successfully authenticated.
* @param request the request which caused the successful authentication
* @param response the response
* @param chain the {@link FilterChain} which can be used to proceed other filters in
* the chain
* @param authentication the Authentication object which was created during
* the authentication process.
* @since 5.2.0
*/
default void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authentication) throws IOException, ServletException {
onAuthenticationSuccess(request, response, authentication);
chain.doFilter(request, response);
}
/**
* Called when a user has been successfully authenticated.
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the Authentication object which was created during
* the authentication process.
*/
void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException;
}
我们可以自定义这个处理器:AuthenticationSuccessHandler
/**
* @author lishisen
* @description 自定义 认证成功后重定向到另一个网站
* @date 2021/12/27 10:04
**/
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 自定义登录的时候拿到一个对象
User user = (User) authentication.getPrincipal();
// 获得登录的用户名
System.out.println(user.getUsername());
// 获得登录的密码,为了安全考虑密码为 null
System.out.println(user.getPassword());
// 获得登录的权限
System.out.println(user.getAuthorities());
response.sendRedirect(url);
}
}
// 控制台输出
admin
null // 密码为了安全起见为 null
[admin, normal] // 这个是在登录的时候给授予的权限
在 http.formLogin() 中使用 .successHandler(new MyAuthenticationSuccessHandler(“http://www.baidu.com”))
启动测试,登录成功后成功跳转到百度
与登录成功处理器类似
创建 MyAuthenticationFailureHandler 处理器类,实现 AuthenticationFailureHandler 接口
/**
* @author lishisen
* @description 自定义登录失败处理器
* @date 2021/12/27 10:26
**/
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
在 http.formPage() 中进行配置
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 必须和表单提交的接口一样,会去执行自定义登录逻辑
.loginProcessingUrl("/login")
// 登录成功后跳转的页面,POST 请求
.successHandler(new MyAuthenticationSuccessHandler("/main.html"))
// 登录失败后跳转的页面
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
/**
* @author lishisen
* @description 403 页面处理
* @date 2021/12/27 11:53
**/
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
// 响应状态
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// 返回 json 格式
response.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员\"}");
writer.flush();
writer.close();
}
}
// 异常处理
http.exceptionHandling().accessDeniedHandler(new MyAccessDeniedHandler());
// 授权
http.authorizeRequests()
// 放行 /error.html,不需要认证
.antMatchers("/error.html").permitAll()
// 放行 /login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
/**
.anyRequest().authenticated();
表示匹配所有的请求,设置全部内容都需要认证
要放到最下面,因为是顺序执行,如果放到第一行的话就是拦截全部的请求了
**/
public C antMatchers(String... antPatterns) {
参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL 规则
.antMathchers("/css/**","/js/**","/images/**").permitAll(); // 放行 js文件夹下所有脚本文件
.antMatchers("/**/*.js").permitAll(); // 只要是 .js 文件都放行
// 授权
http.authorizeRequests()
// 放行 /error.html,不需要认证
.antMatchers("/error.html").permitAll()
// 放行 /login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 放行静态资源
.antMatchers("/images/**", "/css/**").permitAll()
.antMatchers("/**/*.png").permitAll()
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
正则表达式
// 正则表达式
.regexMatchers(".+[.]png").permitAll()
//可以加上请求的method,antMatchers 也可以
.regexMatchers(HttpMethod.POST, ".+[.]png").permitAll()
# 配置文件
spring.mvc.servlet.path=/xxxx
// mvc 匹配
.mvcMatchers("/hello").servletPath("/xxxx").permitAll()
permitAll 允许所有的一个请求
denyAll 禁止
anonymous 只有匿名才能匹配到路径
authenticated 必须要认证
fullyAuthenticated 必须是完全登录了之后才能访问
rememberMe 通过免登录的形式才能访问
Spring Security 中支持很多其他权限控制,这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。
在 http.authorizeRequests() 中添加权限控制语句
// 授权
http.authorizeRequests()
// 放行 /error.html,不需要认证
.antMatchers("/error.html").permitAll()
// 放行 /login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 放行静态资源
.antMatchers("/images/**", "/css/**").permitAll()
.antMatchers("/**/*.png").permitAll()
// 权限控制
.antMatchers("/main1.html").hasAuthority("admin")// " "双引号里面的权限严格区分大小写
// 所有请求都必须认证才能访问,必须登录
.anyRequest().authenticated();
启动测试后有 admin 登录成功会赋予权限,然后请求 main1.html 可以正常访问。如果把admin换成Admin,则会出现 403 的错误,表示权限不够。
可以使用 hasAnyAuthority 设置多个权限值
// 权限控制
.antMatchers("/main1.html").hasAnyAuthority("admin","Admin")
在服务层实现类中添加权限 UserDetailsServiceImpl,
return new User(username, password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_aaa"));
// 注意,这里添加的角色权限必须以 ROLE_ 开头
在 SpringSecurityConfig 中进行配置
// 角色控制
.antMatchers("/main1.html").hasRole("aaa")
// 这里绝对不能写 ROLE_ ,只需要写 _后面的值就可以,严格区分大小写,否则会有 403 权限不够的错误。
// Ip 地址控制
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
上面用到的访问控制的方法都可以使用 access 表达式来实现
// access
.antMatchers("/main1.html").access("hasRole('abc')")
官网地址:https://docs.spring.io/spring-security/site/docs/5.5.4/reference/html5/#el-common-built-in
public interface MyService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
/**
* @author lishisen
* @description 自定义 access 访问控制
* @date 2021/12/27 12:44
**/
@Service
public class MyServiceImpl implements MyService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// 获取主体
Object obj = authentication.getPrincipal();
// 判断主体是否属于 UserDetails
if (obj instanceof UserDetails) {
// 获取权限
UserDetails userDetails = (UserDetails) obj;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
// 判断请求的 URI 是否在权限里
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
// 授权
http.authorizeRequests()
// 放行 /error.html,不需要认证
.antMatchers("/error.html").permitAll()
// 放行 /login.html ,不需要认证
.antMatchers("/login.html").permitAll()
// 放行静态资源
.antMatchers("/images/**", "/css/**").permitAll()
.antMatchers("/**/*.png").permitAll()
// 使用自定义的 access 访问控制
.anyRequest().access("@myServiceImpl.hasPermission(request, authentication)");
启动测试后会发现没有权限不能访问,因为这个自定义中得到的 URI 在授权的时候没有给与权限 /main.html
在 UserDetailsServiceImpl 中给与权限即可
return new User(username, password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_aaa,/main.html"));
Spring Security 中提供了一些访问控制的注解。这些注解都是默认是都不可用的,需要通过 @EnableGlobalMethodSecurity
进行开启后使用
如果设置的条件允许,程序正常执行,如果不允许会报 500 错误。
这些注解可以写到 Service 接口或方法上,也可以写到 Controller 或 Controller 的方法上。通常情况下都是写在控制器方法上的,控制接口 URL 是否允许被访问。
专门用于判断是否具有角色的,能写在方法或类上,参数要以 ROLE_ 开头
使用:
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true) // 默认是false,需要开启才能使用
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
/**
* @description 登录
* @author lishisen
* @date 2021/12/27 8:15
**/
@Secured("ROLE_aaa") // 判断请求是否有这个角色
@RequestMapping("/toMain")
public String login() {
return "redirect:main.html";
}
都是方法或类级别注解
使用:
@EnableGlobalMethodSecurity(prePostEnabled = true)
/**
* @description 登录
* @author lishisen
* @date 2021/12/27 8:15
**/
// 允许角色以 ROLE_ 开头,也可以不以 ROLE_ 开头,严格区分大小写
@PreAuthorize("hasRole('aaa')")
@RequestMapping("/toMain")
public String login() {
return "redirect:main.html";
}
SpringSecurity 中 RememberMe 为“记住我” 功能,用户只需要在登录时添加 remember-me 复选框,取值为 true。SpringSecurity 会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
使用:
添加依赖
Spring Security 实现 RememberMe 功能时底层实现依赖 Spring-JDBC ,所以需要导入 Spring-JDBC。以后多使用 MyBatis 框架而很少直接导入 Spring-JDBC,所以导入 MyBatis 启动器同时添加 MySQL 驱动。
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.16version>
dependency>
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/security?serverTimezone=UTC&zeroDateTimeBehavior=CONVERT_TO_NULL&allowMultiQueries=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 设置数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动建表,第一次使用,第二次关闭
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
// 记住我
http.rememberMe()
// 设置数据源
.tokenRepository(persistentTokenRepository)
// 超时时间,默认两周
.tokenValiditySeconds(60)
// 自定义登录逻辑
.userDetailsService(userDetailsService);
当我们登录的时候,在数据库中的表中都会记录一条记录,上面记录了最后一次登录的时间,如果下次访问网页的时候时间过了就需要重新进行一下登录。
SpringSecurity 可以在一些视图技术中进行控制显示效果。例如:jsp 或者 thymeleaf。在非前后端分离且使用 Spring Boot 的项目中多使用 Thyemeleaf 作为视图展示技术。
Thymeleaf 对 Spring Security 的支持都放在 thymeleaf-extras-springsecurityX
中,所以使用的时候需要引入此 jar 包依赖和 thymeleaf 的依赖。
可以在 html 页面中通过 sec:authentication=""
获取 UsernamePasswordAuthenticationToken
中所有 getXXX
的内容,包含父类中的 getXXX
的内容。
WebAuthenticationDetails
的实例。可以获取 remoteAddress
(客户端 ip)和 sessionId
(当前 sessionId)
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
登录账号:<span sec:authentication="name">span><br/>
登录账号:<span sec:authentication="principal.username">span><br>
凭证:<span sec:authentication="credentials">span><br>
权限和角色:<span sec:authentication="authorities">span><br>
客户端地址:<span sec:authentication="details.remoteAddress">span><br>
sessionId:<span sec:authentication="details.sessionId">span>
body>
html>
@RequestMapping("/demo")
public String toDemo() {
return "demo";
}
访问测试
首先访问登录上去,然后访问 localhost:8080/demo
可以看到一些授权的信息
设置用户角色和权限
设定用户具有 admin、/insert、/delete 权限 ROLE_abc 角色
// 在 UserDetailsServiceImpl 类中设置权限
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_aaa,/main.html,/insert/delete"));
在页面中根据用户权限和角色判断页面中显示的内容
通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增button>
<button sec:authorize="hasAuthority('delete')">删除button>
<button sec:authorize="hasAuthority('update')">修改button>
<button sec:authorize="hasAuthority('select')">查看button>
<br>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增button>
<button sec:authorize="hasRole('abc')">删除button>
<button sec:authorize="hasRole('abc')">修改button>
<button sec:authorize="hasRole('abc')">查看button>
启动测试:
只需要向 Spring Security 项目中发送一个 /logout
请求就可以了
修改之前的 main.html
页面,添加一个退出链接请求,运行后点击退出按钮就可以退出了
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<h1>登录成功h1>
<br>
<h1><a href="main1.html"> 跳转a>h1>
<br>
<a href="/logout"> 退出a>
body>
html>
退出后地址栏的 url 效果不是很好,可以在 SpringSecurityConfig 中添加配置
http.logout()
// 自定义的退出请求路径,一般默认即可
.logoutUrl("/logout")
// 退出成功后跳转的地址
.logoutSuccessUrl("/login.html");
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“OneClick Attack” 或者 Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip 地址,端口中任何一个不相同就是跨域请求
客户端与服务进行交互时,由于 http 协议本身是无状态协议,所以引入了 cookie 进行记录客户端身份。在 cookie 中会存放 session id 用来识别客户端身份的。在跨域的情况下,session id 可能被第三方恶意胁持,通过这个 session id 向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
从 Spring Security 4 开始 CSRF 防护默认开启。默认会拦截请求。进行 CSRF 处理。CSRF 为了保证不是其他第三方网站访问,要求访问时携带参数名为 _csrf 值为 token(token 在服务端产生) 的内容,如果 token 和服务端的 token 匹配成功,则正常访问。
简单说就是我拿到了别人的 session id 去替别人做一些事情
使用:
DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
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>
<button type="submit">登录button>
form>
body>
html>
/**
* @description 跳转到 template 里面的login页面
* @author lishisen
* @date 2021/12/28 8:27
**/
@RequestMapping("/showLogin")
public String toLogin() {
return "login";
}
// 注释掉
// 关闭 csrf 防护
//http.csrf().disable();
提交登录请求后,在控制台可以看到请求的参数会携带 _crsf
后台会进行对比是否是自己给出的 token 的值,如果是则是安全的请求,如果不是则会以为是一个恶意的攻击请求,会阻止请求。
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。
OAUTH 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用 OAUTH 认证服务,任何服务提供商都可以实现自身的 OAUTH 认证服务,因而 OAUTH 是开放的。业界提供了 OAUTH 的多种实现如 PHP、JavaScript、Java、Ruby 等各种语言开发包,大大节约了程序员的时间,因而 OAUTH 是简易的。互联网很多服务如 Open API,很多大公司如 Google,Yahoo,Microsoft 等都提供了 Oauth 认证服务,这些足以证明 OAUTH 标准逐渐称为开放资源授权的标准。
与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。oAuth是Open Authorization的简写。
百度百科:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin
OAUTH协议:https://datatracker.ietf.org/doc/html/rfc6749
网站使用微信认证的过程:
资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证,验证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到网站。
此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码
认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功。
客户端携带令牌访问资源服务器的资源。网站携带令牌请求访问微信服务器获取用户的基本信息。
资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
注意:资源服务器和认证服务器可以是一个服务器也可以分开的服务,如果是分开的服务资源服务器通常要请求认证服务器来校验令牌的合法性。
Oauth2 认证流程:
常用术语:
令牌类型:
特点:
流程:
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR6spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
public class User implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authorities;
public User(String username, String password, List<GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service
public class UserService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String password = passwordEncoder.encode("123456");
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/oauth/**", "/login/**", "/logout/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll()
.and()
.csrf().disable();
}
}
/**
* @author lishisen
* @description 授权服务器
* @date 2021/12/28 11:03
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 客户端ID
.withClient("client")
// 密钥
.secret(passwordEncoder.encode("111222333"))
// 重定向地址
.redirectUris("http://www.baidu.com")
// 授权范围
.scopes("all")
//授权模式: authorization_code:授权码模式
.authorizedGrantTypes("authorization_code");
}
}
/**
* @author lishisen
* @description 资源服务器
* @date 2021/12/28 11:10
**/
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 所有请求都去认证
.anyRequest()
.authenticated()
.and()
// 认证资源
.requestMatchers()
.antMatchers("/user/**");
}
}
启动测试运行
地址栏访问:http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all,进入 Security 内置的登录页面
点击 Authorize 按钮之后在地址栏可以看到一个授权码
使用 postman 请求地址:http://localhost:8080/oauth/token
携带以下参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3JBKgvQL-1640697518420)(D:\notes\SpringSecurity\Spirng Security 基础知识点总结.assets\image-20211228130950887.png)]
结果得到一个进入的 token:access_token
得到 token 之后,使用 postman 请求 http://localhost:8080/user/getCurrentUser 资源地址,并且携带 token参数,得到 Controller 中返回的结果
修改上面的授权服务器的配置类
重写方法:configure(AuthorizationServerEndpointsConfigurer endpoints)
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userService);
}
// 在 SecurityConfig 中注册这个 Bean
@Bean
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
授权控制器的授权模式中增加一项 password
启动测试:
在请求的时候携带 username 和 password,结果会有一个 token,通过这个 token 可以访问到资源的信息
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
spring.redis.host=127.0.0.1
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
@Autowired
@Qualifier("redisTokenStore")
private TokenStore tokenStore;
/**
* @description 密码模式
* @author lishisen
* @date 2021/12/28 13:44
**/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userService)
.tokenStore(tokenStore);
}
启动测试
正常的使用用户名和密码进行请求,然后查看 Redis 中
HTTP Basic Auth 简单点说明就是每次请求 API 时都提供用户的 username和 password,Basic Auth 是配合 RESTful API 使用的最简单的认证方式,只需提供用户密码即可,但由于把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少,因此,在开发对外开放的 RESTful API 时,尽量避免采用 HTTP Basic Auth。
Cookie 认证机制就是为一次请求认证在服务端创建一个 Sessiion 对象,同时在客户端的浏览器端创建了一个 Cookie 对象;通过客户端带上来 Cookie 对象来与服务器端的 session 对象匹配来实现状态管理的。默认的,当我们关闭浏览器器的时候,cookie 会被删除。但可以通过修改 cookie 的 expire time 使 cookie 在一定时间内有效。
OAuth(开放授权,Open Authorization)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一 web 服务上存储的私密资源(如照片、视频、联系人列表),而无需将用户名和密码提供给第三方应用。如网站通过微信、微博登录等,主要用于第三方登录。
OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如:视频编辑网站)在特定的时段(例如,接下来的2个小时内)内访问特定的资源(例如,仅仅是某一相册中的视频)。这样 OAuth 让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录,大概的流程是这样的:
Token Auth 的优点(Token 机制相对于 Cookie 机制又有什么好处?)
什么是 JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),他定义了一种简介的、自包含的协议格式,用于在通信双方传递 json 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公钥/私钥对来签名,防止被篡改。
JWT令牌的优点: jwt 基于 json,非常方便解析;可以在令牌中自定义丰富的内容,易扩展;通过非对称加密算法及数字签名技术,JWT 防止篡改,安全性高;资源服务使用 JWT 可不依赖认证服务即可完成授权。
缺点: JWT 令牌较长,占存储空间比较大
**JWT 组成:**一个 JWT 实际上就是一个字符串,三部分组成:头部、载荷与签名。
头部用于描述关于该 JWT 的最基本的信息,例如其类型(即 JWT)以及签名所用的算法(如 HMAC SHA 256 或 RSA)等,这也可以被表示成一个 JSON 对象。
{
"alg": "HS256",
"typ": "JWT"
}
对头部的 json 字符串进行 BASE64 编码,编码后的字符串如下: ewogICAgImFsZyI6ICJIUzI1NiIsCiAgICAidHlwIjogIkpXVCIKfQ==
BASE64 是一种基于 64 个可打印字符来表示二进制数据的表示方法,由于 2 的 6 次方等于 64,所以每 6 个比特为一个单元,对应某个可打印字符。三个字节有 24 个比特,对应与 4 个 BASE64 单元,即 3 个字节需要 4 个可打印字符来表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用他们可以非常方便的完成基于 BASE64 的编码和解码。
第二部分是负载,就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包括三个部分:
iss: jwt 签发者
sub: jwt 所面向的用户
aud: 接受 jwt 的一方
exp: jwt 的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该 jwt 都是不可用的
iat: jwt 的签发时间
jti: jwt 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击。
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为该部分在客户端可解密。
私有声明是 提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息
这个指的就是自定义的 claim。比如下面那个举例中的 name 都属于自定义的claim。这些 claim 跟 JWT 标准规定的 claim 区别在于:JWT 规定的 claim,JWT 的接收方在拿到 JWT 之后,都知道怎么对这些标准的 claim 进行验证(还不知道是否能够验证);而 private claims 不会验证,除非明确告诉接收方要对这些 claim 进行验证以及规则才行。
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
其中 sub 是标准的声明,name 是自定义的声明(公共的或私有的),然后将其进行 base64 编码,得到 Jwt 的第二部分
ewogICAgInN1YiI6ICIxMjM0NTY3ODkwIiwKICAgICJuYW1lIjogIkpvaG4gRG9lIiwKICAgICJpYXQiOiAxNTE2MjM5MDIyCn0=
这个声明中尽量不要放一些敏感的信息
JWT 的第三部分是一个签证信息,这个签证信息由三个部分组成:
这个部分需要 base64 加密后的 header 和base64 加密后的 payload 使用,连接组成的字符串,然后通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
将这三部分用 . 连接成一个完成的字符串,构成了最终的 jwt
注意:secret 是保存在服务器端的,jwt 的签发生成也是在服务器端的,secret 就是用来进行 jwt 的签发和 jwt 的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个 secret,那就意味着客户端是可以自我签发 jwt 了。
JJWT 是一个提供端到端的JWT 创建和验证的 Java 库,永远免费和开源(Apache License,版本 2.0),JJWT 很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
规范官网:https://jwt.io/
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
@Test
void contextLoads() {
JwtBuilder jwtBuilder = Jwts.builder()
// 唯一ID{“id": "888"}
.setId("888")
// 接收的用户 {"sub": "Zhangsan"}
.setSubject("Zhangsan")
// 签发时间 {"iat": "..."}
.setIssuedAt(new Date())
// 签名算法,及密钥
.signWith(SignatureAlgorithm.HS256, "xxxx");
String compact = jwtBuilder.compact();
System.out.println(compact);
System.out.println("--------------------------------");
String[] split = compact.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
// 解析 token
@Test
void tokenParse() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJaaGFuZ3NhbiIsImlhdCI6MTY0MDY3ODAzM30.42FtM2rhB0DU-X2OdnMHw8Gdy5-jKYuL4UuC35JIe_A";
// 解析 token,获取 Claims jwt 中荷载申明的对象
Claims xxxx = (Claims) Jwts.parser()
// 密钥
.setSigningKey("xxxx")
.parse(token)
.getBody();
System.out.println(xxxx.getId());
System.out.println(xxxx.getSubject());
System.out.println(xxxx.getIssuedAt());
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-syRFiygC-1640697518424)(D:\notes\SpringSecurity\Spirng Security 基础知识点总结.assets\image-20211228160609766.png)]
很多时候 token 是不能够永久生效的,所以需要给下发的 token 添加一个过期时间。原因:从服务器发出的 token,服务器自己并不做记录,就存在一个弊端就是,服务端无法主动控制某 token 的立刻生效。
/**
* @description 带有过期时间的jwt
* @author lishisen
* @date 2021/12/28 16:13
**/
@Test
void JwtHasExipre() {
// 当前时间
long date = System.currentTimeMillis();
// 失效时间
long exp = date + 60 * 1000;
JwtBuilder jwtBuilder = Jwts.builder()
// 唯一ID{“id": "888"}
.setId("888")
// 接收的用户 {"sub": "Zhangsan"}
.setSubject("Zhangsan")
// 签发时间 {"iat": "..."}
.setIssuedAt(new Date())
// 签名算法,及密钥
.signWith(SignatureAlgorithm.HS256, "xxxx")
// 设置失效时间
.setExpiration(new Date(exp));
String compact = jwtBuilder.compact();
System.out.println(compact);
System.out.println("--------------------------------");
String[] split = compact.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
// 失效后访问令牌会抛出异常
io.jsonwebtoken.ExpiredJwtException: JWT expired at 2021-12-28T16:18:21Z. Current time: 2021-12-28T16:18:28Z, a difference of 7072 milliseconds. Allowed clock skew: 0 milliseconds.
// 以键值对的方式进行声明
.claim("name", "zhao")
.claim("photo","aaa.jpg");
// 解析,通过键取
System.out.println(xxxx.get("name"));
System.out.println(xxxx.get("photo"));
在上面的 SpringSecurity 2 中使用的是 Redis 保存的授权码,换成 JWT 之后不在需要使用 Redis 进行存储了,因为 JWT 是无状态的。
使用:
/**
* @author lishisen
* @description JWT 令牌配置
* @date 2021/12/28 16:33
**/
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
// 设置 jwt 密钥
jwtAccessTokenConverter.setSigningKey("test_key");
return jwtAccessTokenConverter;
}
}
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* @description 密码模式
* @author lishisen
* @date 2021/12/28 13:44
**/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager).userDetailsService(userService)
// accessToken转成JWTtoken
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
}
/**
* @author lishisen
* @description TODO
* @date 2021/12/28 17:18
**/
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
// 放一些扩展的信息
HashMap<String, Object> map = new HashMap<>();
map.put("enhance","enhancer info");
((DefaultOAuth2AccessToken)oAuth2AccessToken).setAdditionalInformation(map);
return oAuth2AccessToken;
}
}
// 需要向容器中注入一个这个bean
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* @description 密码模式
* @author lishisen
* @date 2021/12/28 13:44
**/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 设置增强内容
TokenEnhancerChain chain = new TokenEnhancerChain();
ArrayList<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
chain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManager).userDetailsService(userService)
// accessToken转成JWTtoken
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(chain);
}
修改 /user/getCurrentUser 请求用于得到 Token 进行解析
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
String header = request.getHeader("Authorization");
String token = header.substring(header.lastIndexOf("bearer") + 7);
return Jwts.parser()
.setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
}
}
使用 postman 进行请求
首先获得令牌
请求的时候设置请求头
首先在授权服务器AuthorizationServerConfig配置中设置令牌的过期时间
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
// 客户端ID
.withClient("client")
// 密钥
.secret(passwordEncoder.encode("111222333"))
// 重定向地址
.redirectUris("http://www.baidu.com")
// 设置令牌过期时间
.accessTokenValiditySeconds(60)
// 刷新令牌失效时间
.refreshTokenValiditySeconds(6000)
// 授权范围
.scopes("all")
//授权模式: authorization_code:授权码模式
// "refresh_token" 设置刷新令牌
.authorizedGrantTypes("authorization_code","password","refresh_token");
}
运行测试的时候,当我们请求令牌的时候就会多一个刷新令牌的键值对
当令牌过期的时候我们使用之前的令牌就不能进行访问了
然后通过设置参数和上面得到的刷新令牌进行访问就可以得到新的令牌了
单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
单点系统需要有一个单独的认证系统,所有的应用系统都需要去找这个认证系统。
认证系统有两个非常重要的作用:
使用之前的一个认证系统 security-oauth2 工程中的 AuthorizationServerConfig(认证系统/授权服务器)
修改 认证系统的 AuthorizationServerConfig 配置类
// 重定向地址
.redirectUris("http://localhost:8081")
// 自动授权
.autoApprove(true)
// 重写方法
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 获取密钥必须要身份认证,单点登录必须要配置
security.tokenKeyAccess("isAuthenticated()");
}
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Greenwich.SR6spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
server:
port: 8081
# 防止 Cookie 冲突,冲突会导致登录验证不通过
servlet:
session:
cookie:
name: OAUTH2-CLIENT-SESSIONID01
# 授权服务器地址
oauth2-server-url: http://localhost:8080
# 授权服务器对应的配置
security:
oauth2:
client:
client-id: client
client-secret: 111222333
user-authorization-uri: ${oauth2-server-url}/oauth/authorize
access-token-uri: ${oauth2-server-url}/oauth/token
resource:
jwt:
key-uri: ${oauth2-server-url}/oauth/token_key
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
}
@EnableOAuth2Sso
@SpringBootApplication
// 开启单点登录
@EnableOAuth2Sso
public class Oauth2client01Application {
public static void main(String[] args) {
SpringApplication.run(Oauth2client01Application.class, args);
}
}
启动测试
首先启动认证系统服务,然后启动客户端服务
地址栏访问 http://localhost:8081/user/getCurrentUser 的时候会自动跳转到 http://localhost:8080/login 请求
这里的用户名密码是认证服务器 UserService 中配置的 UserDetails
账号密码输入正确认证通过后又会跳转到 http://localhost:8081/user/getCurrentUser,单点登录成功
本视频参考学习自:https://www.bilibili.com/video/BV1CY411p7dT?p=1