认证:系统认为用户是否能登录
授权:系统判断用户是否有权限去做某件事情
https://blog.csdn.net/zhanduo0118/article/details/112093781
SpringSecurity入门
项目代码
https://gitee.com/galen.zhang/spring-security-demo
创建Maven项目spring-security-demo,引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
创建静态文件
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form method="post" action="/login">
username:<input type="text" name="username"><br>
password:<input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
</body>
</html>
main.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Main</title>
</head>
<body>
Login success!
</body>
</html>
启动springboot应用,访问 http://localhost:8080/login.html
默认跳转到spring security的登录页,用户名:user,密码在控制台中输出
自定义登录逻辑
实现UserDetailsService接口,根据用户名查询用户信息
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在!");
}
String password = passwordEncoder.encode("123");
// 角色需要以ROLE_开头
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_normal,/insert,/update"));
}
}
自定义登录页面
https://www.shuzhiduo.com/A/nAJvZMyoJr/
Spring Security即将弃用WebSecurityConfigurerAdapter配置类
https://blog.csdn.net/donglinjob/article/details/108854574
Spring Security(四) 认证过程常用配置
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
// 登录成功后跳转页面,必须是Post请求
.defaultSuccessUrl("/toMain")
.loginPage("/login.html");
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
return http.build();
}
}
@Controller
public class LoginController {
// 跳转到登录成功页面,必须是POST请求
@RequestMapping("toMain")
public String toMain() {
return "redirect:main.html";
}
}
自定义失败页面
添加fail.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
操作失败,请重新登录 <a href="/login.html">跳转</a>
</body>
</html>
配置类
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
.defaultSuccessUrl("/toMain")
// 登录失败后跳转页面,必须是Post请求
.failureForwardUrl("/toFail")
;
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
页面跳转
@Controller
public class LoginController {
@RequestMapping("toMain")
public String toMain() {
System.out.println("toMain.....");
return "redirect:main.html";
}
@RequestMapping("toFail")
public String toFail() {
System.out.println("toFail.....");
return "redirect:fail.html";
}
}
自定义登录用户名参数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form method="post" action="/login">
username:<input type="text" name="uname"><br>
password:<input type="password" name="pwd"><br>
<input type="submit" value="Login">
</form>
</body>
</html>
配置类
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
.usernameParameter("uname")
.passwordParameter("pwd")
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
.defaultSuccessUrl("/toMain")
// 登录失败后跳转页面,必须是Post请求
.failureForwardUrl("/toFail")
;
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义登录成功处理器
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println(request.getRemoteAddr());
User user = (User) authentication.getPrincipal();
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getAuthorities());
response.sendRedirect(url);
}
}
配置类
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
.usernameParameter("uname")
.passwordParameter("pwd")
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
//.defaultSuccessUrl("/toMain")
//自定义成功处理器,与defaultSuccessUrl不能同时使用
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
// 登录失败后跳转页面,必须是Post请求
.failureForwardUrl("/toFail")
;
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义登录失败处理器
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);
}
}
配置类
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
.usernameParameter("uname")
.passwordParameter("pwd")
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
//.defaultSuccessUrl("/toMain")
//自定义成功处理器,与defaultSuccessUrl不能同时使用
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
// 登录失败后跳转页面,必须是Post请求
//.failureForwardUrl("/toFail")
.failureHandler(new MyAuthenticationFailureHandler("/fail.html"))
;
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
antMatcher路径匹配
https://blog.csdn.net/zhanduo0118/article/details/112093802
SpringSecurity学习记录3
? 匹配一个字符
* 匹配0个或多个字符
** 匹配0个或多个目录
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// .antMatchers("/fail.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 按后缀指定放行文件
.antMatchers("/**/*.png").permitAll()
.anyRequest().authenticated();
regexMatcher正则表达式匹配
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// .antMatchers("/fail.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 匹配.png后缀的文件
.regexMatchers(".+[.]png").permitAll()
.regexMatchers(HttpMethod.POST, "/demo").permitAll()
.anyRequest().authenticated();
mvcMatchers匹配servletPath
application.properties里面加上
spring.mvc.servlet.path=/spring-intmall
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// .antMatchers("/fail.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 如果指定了servletContext路径(项目名称),需要加上servletPath
.mvcMatchers("/demo").servletPath("/spring-intmall").permitAll()
.anyRequest().authenticated();
内置控制访问方法
ExpressionUrlAuthorizationConfigurer包含6种访问方式:
permitAll、denyAll、anonymous、authenticated、fullyAuthenticated、rememberMe
权限判断:hasAuthority("admin") / hasAnyAuthority("admin", "adminN")
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").permitAll()
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 权限区分大小写
// .antMatchers("/main1.html").hasAuthority("admin")
.antMatchers("/main1.html").hasAnyAuthority("admin", "adminN")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
角色判断
- 在返回的用户中添加角色
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!"admin".equals(username)) {
throw new UsernameNotFoundException("用户名不存在!");
}
String password = passwordEncoder.encode("123");
// 角色需要以ROLE_开头
return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_normal"));
}
}
- 角色权限判断中不能加前缀ROLE_
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").permitAll()
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 权限区分大小写
// .antMatchers("/main1.html").hasAuthority("admin")
// .antMatchers("/main1.html").hasAnyAuthority("admin", "adminN")
// 角色校验会自动添加ROLE_,这里不能再加前缀;区分大小写
// .antMatchers("/main1.html").hasRole("normal")
.antMatchers("/main1.html").hasAnyRole("normal", "Normal")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
IP地址限制
request.getRemoteAddr():localhost输出的IP地址是 0:0:0:0:0:0:0:1
127.0.0.1和ipconfig获取的ip,在设置hasIpAddress是不一样的
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").permitAll()
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 权限区分大小写
// .antMatchers("/main1.html").hasAuthority("admin")
// .antMatchers("/main1.html").hasAnyAuthority("admin", "adminN")
// 角色校验会自动添加ROLE_,这里不能再加前缀;区分大小写
// .antMatchers("/main1.html").hasRole("normal")
// .antMatchers("/main1.html").hasAnyRole("normal", "Normal")
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
自定义403处理方案
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type", "application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\": \"error\", \"msg\": \"权限不足,请联系管理员\"}");
writer.flush();
writer.close();
}
}
配置类
@Configuration
public class SecurityConfig {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
.usernameParameter("uname")
.passwordParameter("pwd")
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
//.defaultSuccessUrl("/toMain")
//自定义成功处理器,与defaultSuccessUrl不能同时使用
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
// 登录失败后跳转页面,必须是Post请求
//.failureForwardUrl("/toFail")
.failureHandler(new MyAuthenticationFailureHandler("/fail.html"))
;
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
access自定义方法实现权限控制
https://blog.csdn.net/donglinjob/article/details/108856052
Spring Security(七) 基于表达式/注解的访问控制
public interface MyService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
@Service
public class MyServiceImpl implements MyService {
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
Object obj = authentication.getPrincipal();
if (obj instanceof UserDetails) {
UserDetails userDetails = (UserDetails) obj;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
配置类
@Configuration
public class SecurityConfig {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
.usernameParameter("uname")
.passwordParameter("pwd")
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
.defaultSuccessUrl("/toMain")
//自定义成功处理器,与defaultSuccessUrl不能同时使用
// .successHandler(new MyAuthenticationSuccessHandler("main.html"))
// 登录失败后跳转页面,必须是Post请求
.failureForwardUrl("/toFail")
// .failureHandler(new MyAuthenticationFailureHandler("/fail.html"))
;
// 认证授权
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// .antMatchers("/fail.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
.antMatchers("/js/**", "/css/**", "/images/**").permitAll()
// 如果指定了servletContext路径(项目名称),需要加上servletPath
// .mvcMatchers("/demo").servletPath("/xxxx").permitAll()
// .antMatchers("/xxxx/demo").permitAll()
// 权限区分大小写
// .antMatchers("/main1.html").hasAuthority("admin")
// .antMatchers("/main1.html").hasAnyAuthority("admin", "adminN")
// 角色校验会自动添加ROLE_,这里不能再加前缀;区分大小写
// .antMatchers("/main1.html").hasRole("normal")
// .antMatchers("/main1.html").hasAnyRole("normal", "Normal")
// .antMatchers("/main1.html").hasIpAddress("127.0.0.1")
// 所有请求都必须被认证(登录后访问)
//.anyRequest().authenticated()
.anyRequest().access("@myServiceImpl.hasPermission(request, authentication)")
;
http.csrf().disable();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
return http.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
基于注解的访问控制
@Secured判断是否具有角色,参数以ROLE_开头。如果权限不足,返回403错误
启用@Secured注解:在springboot启动类上增加注解
@EnableGlobalMethodSecurity(securedEnabled = true)
//controller方法中增加注解
@Secured("ROLE_normal")
@RequestMapping("toMain")
public String toMain() {
System.out.println("toMain.....");
return "redirect:main.html";
}
@PreAuthorize表示访问方法或类之前判断权限
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@PreAuthorize("hasRole('normal')")
@RequestMapping("toMain")
public String toMain() {
System.out.println("toMain.....");
return "redirect:main.html";
}
RememberMe功能
自动把用户信息存储到数据库中
引入mybatis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
配置数据库
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security
spring.datasource.username=root
spring.datasource.password= root
spring.main.allow-circular-references=true
在页面上加入remeber-me复选框
rememberMe:<input type="checkbox" name="remember-me"><br>
配置类
@Configuration
public class SecurityConfig {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 表单提交自定义登录页
http.formLogin()
.usernameParameter("uname")
.passwordParameter("pwd")
// 登录提交处理请求的地址
.loginProcessingUrl("/login")
.loginPage("/login.html")
// 登录成功后跳转页面,必须是Post请求
//.defaultSuccessUrl("/toMain")
//自定义成功处理器,与defaultSuccessUrl不能同时使用
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
// 登录失败后跳转页面,必须是Post请求
//.failureForwardUrl("/toFail")
.failureHandler(new MyAuthenticationFailureHandler("/fail.html"))
;
// 认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/fail.html").access("permitAll")
// 所有请求都必须被认证(登录后访问)
.anyRequest()
.authenticated();
http.csrf().disable();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
http.rememberMe()
// token失效时间,秒
.tokenValiditySeconds(600)
// form表单字段名称
// .rememberMeParameter("rememberMe")
.tokenRepository(getPersistentTokenRepository())
.userDetailsService(userDetailsService);
return http.build();
}
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 自动建表,第一次启动时候需要,第二次启动需要注释掉
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
Thymeleaf中Spring Security的使用
https://blog.csdn.net/donglinjob/category_10352718.html
安全管理框架(Spring Security、Shiro)
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
在template目录中创建文件demo.html
<!DOCTYPE html>
<html xmlns= "http://www.w3.org/1999/xhtml"
xmlns: th= = "http://www.thymeleaf.org"
xmlns: sec= = "http://www.thy meleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset= "UTF-8">
<title>Title</ title>
</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><br/>
通过权限判断:
<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('normal')">新增</button>
<button sec:authorize="hasRole('normal')">删除</button>
<button sec:authorize="hasRole('normal')">修改</button>
</ body>
</html
退出登录
实现退出非常简单,只要在页面中添加/logout的超链接即可
<a href="/logout">退出</a>
指定退出后的跳转页面
http.logout()
// 退出登录跳转页面
.logoutSuccessUrl("/login.html");
Oauth2认证
https://blog.csdn.net/CM134cmcm6513/article/details/120543191
认证解决方案
第三方认证技术方案最主要是解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间要遵循一定的接口协议。
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务
Oauth2认证中包括的角色:
客户端 :本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源
资源拥有者 :通常为用户,也可以是应用程序,即该资源的拥有者
授权服务器(也称认证服务器):用来对资源拥有的身份进行认证、对访问资源进行授权。 客户端要想访问资源需要通过认证服务器由资源拥有者授 权后方可访问。
资源服务器 :存储资源的服务器,比如,畅购用户管理服务器存储了畅购的用户信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。
Authorize Endpoint :授权端点,进行授权
Token Endpoint :令牌端点,经过授权拿到对应的Token
lntrospection Endpoint :校验端点,校验Token的合法性
Revocation Endpoint :撤销端点,撤销授权
https://blog.51cto.com/u_15359644/3803760
Spring Security + OAuth2.0
https://www.modb.pro/db/223918
Spring Cloud学习笔记——Spring Security与OAuth2
Spring Security Oauth2 demo
项目代码
https://gitee.com/galen.zhang/spring-security-oauth2-demo
创建maven项目spring-security-oauth2-demo
引入依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<!-- OAuth2 自动配置-->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<!-- OAuth2资源服务器-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
根据用户名从数据库中加载用户信息
@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("admin", password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
Spring Security中对Oauth2路径忽略权限校验
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**", "/login/**", "/logout/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll();
}
/**
* 注意在@EnableAuthorizationServer -> AuthorizationServerConfigurerAdapter里面会自动创建一个WebSecurityConfigurerAdapter实例
* 在创建SecurityFilterChain时,会出现错误:Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.
*/
// @Bean
// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http.csrf().disable()
// .authorizeRequests()
// .antMatchers("/oauth/**", "/login/**", "/logout/**")
// .permitAll()
// .anyRequest()
// .authenticated()
// .and()
// .formLogin()
// .permitAll();
// return http.build();
// }
}
授权服务器配置
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin")
.secret(passwordEncoder.encode("112233"))
.accessTokenValiditySeconds(3600)
.redirectUris("http://www.baidu.com")
.scopes("all")
.authorizedGrantTypes("authorization_code")
;
}
}
资源服务器配置
/**
* 资源服务器
*/
@Configuration
@EnableResourceServer
public class ResourServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/user/**")
;
}
}
demo获取数据
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 获取当前用户
* @param authentication
* @return
*/
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication.getPrincipal();
}
}
先获取授权码
http://localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_uri=http://www.baidu.com&scope=all
admin/123456
获取token
http://localhost:8080/oauth/token
PostMan,post方法,Authorization -> Basic Auth: admin/112233
Body -> x-www-form-urlencoded
grant_type=authorization_code
code=
client_id=admin
redirect_uri=http://www.baidu.com
scope=all
获取数据
http://localhost:8080/user/getCurrentUser
Authorization -> Bearer Token
注意在@EnableAuthorizationServer -> AuthorizationServerConfigurerAdapter里面会自动创建一个WebSecurityConfigurerAdapter实例
而在自定义的SecurityConfig类中如果使用到了SecurityFilterChain,会出现错误:Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.
密码模式
授权服务器配置类
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
/**
* 使用密码模式所需配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin")
.secret(passwordEncoder.encode("112233"))
.accessTokenValiditySeconds(3600)
.redirectUris("http://www.baidu.com")
.scopes("all")
// .authorizedGrantTypes("authorization_code")
.authorizedGrantTypes("password")
;
}
}
配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**", "/login/**", "/logout/**")
.permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll();
}
/**
* 注意在@EnableAuthorizationServer -> AuthorizationServerConfigurerAdapter里面会自动创建一个WebSecurityConfigurerAdapter实例
* 在创建SecurityFilterChain时,会出现错误:Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.
*/
// @Bean
// SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// http.csrf().disable()
// .authorizeRequests()
// .antMatchers("/oauth/**", "/login/**", "/logout/**")
// .permitAll()
// .anyRequest()
// .authenticated()
// .and()
// .formLogin()
// .permitAll();
// return http.build();
// }
}
密码模式获取token
http://localhost:8080/oauth/token
PostMan,post方法,Authorization -> Basic Auth: admin/112233
Body -> x-www-form-urlencoded
grant_type=password
username=admin
password=123456
scope=all
再次查询
http://localhost:8080/user/getCurrentUser
Redis存储Token
引入pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis依赖commons-pool 这个依赖一定要添加 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Redis配置类
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
授权服务器配置类
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
@Qualifier("redisTokenStore")
private TokenStore tokenStore;
/**
* 使用密码模式所需配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
.tokenStore(tokenStore)
;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin")
.secret(passwordEncoder.encode("112233"))
.accessTokenValiditySeconds(3600)
.redirectUris("http://www.baidu.com")
.scopes("all")
// .authorizedGrantTypes("authorization_code")
.authorizedGrantTypes("password")
;
}
}
jwt
https://jwt.io/
引入pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
测试用例
@Test
public void testCreateToken() {
JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识{"jti": "8888"}
.setId("8888")
// 主体,用户{"sub": "Rose"}
.setSubject("Rose")
// {"ita": "xxxx"}
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "xxxx");
// 获取token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("====================");
String[] arr = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(arr[0]));
System.out.println(Base64Codec.BASE64.decodeToString(arr[1]));
System.out.println(Base64Codec.BASE64.decodeToString(arr[2]));
}
@Test
public void parseToken() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY1ODg0OTQxMX0.mTKa5-7ko88bvz7X1yZD5LBvvlZ-KznteLNfpO_6LN0";
// 解析token
Claims claims = Jwts.parser().setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println("id: " + claims.getId());
System.out.println("subject: " + claims.getSubject());
System.out.println("issueAt: " + claims.getIssuedAt());
}
/**
* 失效时间
*/
@Test
public void testCreateTokenHasExpiration() {
long now = System.currentTimeMillis();
// 过期时间1分钟
long exp = now + 60 * 1000;
JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识{"jti": "8888"}
.setId("8888")
// 主体,用户{"sub": "Rose"}
.setSubject("Rose")
// {"ita": "xxxx"}
.setIssuedAt(new Date())
// 设置过期时间
.setExpiration(new Date(exp))
.signWith(SignatureAlgorithm.HS256, "xxxx");
// 获取token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("====================");
String[] arr = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(arr[0]));
System.out.println(Base64Codec.BASE64.decodeToString(arr[1]));
System.out.println(Base64Codec.BASE64.decodeToString(arr[2]));
}
/**
* token如果失效,在parseClaimsJws方法中会抛出异常:
* io.jsonwebtoken.ExpiredJwtException: JWT expired at 2022-07-27T19:59:08Z.
*/
@Test
public void parseTokenHasExpiration() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY1ODkyMzA4OCwiZXhwIjoxNjU4OTIzMTQ4fQ.iI_lyvD4Ji3qN2t4-rQdz_fjs8ACuuJFWGWvy1poZYc";
// 解析token
Claims claims = Jwts.parser().setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println("id: " + claims.getId());
System.out.println("subject: " + claims.getSubject());
System.out.println("issueAt: " + claims.getIssuedAt());
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("签发时间:" + simpleDateFormat.format(claims.getIssuedAt()));
System.out.println("过期时间:" + simpleDateFormat.format(claims.getExpiration()));
System.out.println("当前时间:" + simpleDateFormat.format(new Date()));
}
/**
* 自定义属性
*/
@Test
public void testCreateTokenByClaims() {
JwtBuilder jwtBuilder = Jwts.builder()
// 声明的标识{"jti": "8888"}
.setId("8888")
// 主体,用户{"sub": "Rose"}
.setSubject("Rose")
// {"ita": "xxxx"}
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "xxxx")
// 自定义属性
.claim("roles", "admin")
.claim("logo", "intmall.jpg")
//直接传入map
//.addClaims(map)
;
// 获取token
String token = jwtBuilder.compact();
System.out.println(token);
System.out.println("====================");
String[] arr = token.split("\\.");
System.out.println(Base64Codec.BASE64.decodeToString(arr[0]));
System.out.println(Base64Codec.BASE64.decodeToString(arr[1]));
System.out.println(Base64Codec.BASE64.decodeToString(arr[2]));
}
/**
* 解析自定义属性
*/
@Test
public void parseTokenHasByClaims() {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODg4Iiwic3ViIjoiUm9zZSIsImlhdCI6MTY1ODkyMzc4MCwicm9sZXMiOiJhZG1pbiIsImxvZ28iOiJpbnRtYWxsLmpwZyJ9.v7ig1_6wg4V4U6zebFWI0lAKsbeMOEewxpr5v4a5IKA";
// 解析token
Claims claims = Jwts.parser().setSigningKey("xxxx")
.parseClaimsJws(token)
.getBody();
System.out.println("id: " + claims.getId());
System.out.println("subject: " + claims.getSubject());
System.out.println("issueAt: " + claims.getIssuedAt());
System.out.println("roles:" + claims.get("roles"));
System.out.println("logo:" + claims.get("logo"));
}
Spring Security Oauth2整合jwt
创建jwt token配置类
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// 配置签名密钥
accessTokenConverter.setSigningKey("test_key");
return accessTokenConverter;
}
}
授权服务器配置
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 使用密码模式所需配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
// 配置存储令牌策略
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin")
.secret(passwordEncoder.encode("112233"))
.accessTokenValiditySeconds(3600)
.redirectUris("http://www.baidu.com")
.scopes("all")
// .authorizedGrantTypes("authorization_code")
.authorizedGrantTypes("password")
;
}
}
使用密码模式做测试:
密码模式获取token
http://localhost:8080/oauth/token
PostMan,post方法,Authorization -> Basic Auth: admin/112233
Body -> x-www-form-urlencoded
grant_type=password
username=admin
password=123456
scope=all
获取到jwt格式的token,可以到 https://jwt.io/ 解析查看令牌里面的数据
再次查询
http://localhost:8080/user/getCurrentUser
扩展jwt中存储的内容
/**
* jwt内容增强器
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("customParam", "testData");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
授权服务器配置
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* 使用密码模式所需配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置jwt内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
// 配置存储令牌策略
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain)
;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin")
.secret(passwordEncoder.encode("112233"))
.accessTokenValiditySeconds(3600)
.redirectUris("http://www.baidu.com")
.scopes("all")
.authorizedGrantTypes("password")
;
}
}
使用密码模式做测试,把token拿到 https://jwt.io/ 做解析
在代码中解析jwt自定义内容
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 获取当前用户
* @param authentication
* @return
*/
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
String head = request.getHeader("Authorization");
String token = head.substring("Bearer ".length());
// return authentication.getPrincipal();
return Jwts.parser().setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
}
}
刷新令牌RefreshToken
授权服务器配置中增加refresh_token
/**
* 授权服务器
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
/**
* 使用密码模式所需配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置jwt内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userService)
// 配置存储令牌策略
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain)
;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("admin")
.secret(passwordEncoder.encode("112233"))
.accessTokenValiditySeconds(3600)
.redirectUris("http://www.baidu.com")
.scopes("all")
.authorizedGrantTypes("password", "refresh_token", "authorization_code")
;
}
}
使用密码模式做测试,获取到token,refresh_token
使用refresh_token获取新的令牌
http://localhost:8080/oauth/token
PostMan,post方法,Authorization -> Basic Auth: admin/112233
Body -> x-www-form-urlencoded
grant_type=refresh_token
refresh_token=
Spring Security Oauth2整合SSO
https://blog.csdn.net/CM134cmcm6513/article/details/120543191
认证解决方案
创建一个maven子项目spring-security-oauth2-client
修改配置文件application.properties
server.port=8081
#防止Cookie冲突,冲突会导致登录验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-DEMO1
#授权服务器地址
oauth2-server-url: http://localhost:8080
#与授权服务器对应的配置
security.oauth2.client.client-id=admin
security.oauth2.client.client-secret=112233
security.oauth2.client.user-authorization-uri=${oauth2-server-url}/oauth/authorize
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key
在启动类上添加@EnableOAuth2Sso注解来启用单点登录功能
@SpringBootApplication
@EnableOAuth2Sso
public class Oauth2ClientDemoApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2ClientDemoApplication.class, args);
}
}
添加接口用于获取当前登录用户信息
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication) {
return authentication;
}
}
修改认证服务器配置
修改授权服务器中的AuthorizationServerConfig类,将绑定的跳转路径为
http://localhost:8081/login,并添加获取秘钥时的身份认证
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//配置client_id
.withClient("admin")
//配置client-secret
.secret(passwordEncoder.encode("112233"))
//配置访问token的有效期
.accessTokenValiditySeconds(3600)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirect_uri,用于授权成功后跳转
// .redirectUris("http://www.baidu.com")
//单点登录时配置
.redirectUris("http://localhost:8081/login")
//配置申请的权限范围
.scopes("all")
//自动授权配置
.autoApprove(true)
//配置grant_type,表示授权类型
.authorizedGrantTypes("password", "refresh_token", "authorization_code");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
// 获取密钥需要身份认证,使用单点登录时必须配置
security.tokenKeyAccess("isAuthenticated()");
}
测试
启动授权服务和客户端服务
访问客户端需要授权的接口 http://localhost:8081/user/getCurrentUser
会跳转到授权服务的登录界面 http://localhost:8080/login
用户名密码admin/123456
授权后会跳转到原来需要权限的接口地址,展示登录用户信息