轻松上手SpringSecurity,OAuth,JWT

Posted Serendipity sn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了轻松上手SpringSecurity,OAuth,JWT相关的知识,希望对你有一定的参考价值。

学习目标

在这里插入图片描述

一.SpringSecurity

1.SpringSecurity简介及快速入门

<1>.SpringSecurity简介

Spring Security是一个高度自定义的安全框架。利用 Spring IoC/DI和AOP功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用 Spring Secruity 的原因有很多,但大部分都是发现了javaEE的 Servlet 规范或 EJB 规范中的安全功能缺乏典型企业应用场景。同时认识到他们在 WAR 或 EAR 级别无法移植。因此如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用 Spring Security解决了这些问题,也为你提供许多其他有用的、可定制的安全功能。正如你可能知道的两个应用程序的两个主要区域是“认证”和“授 权”(或者访问控制)。这两点也是 Spring Security 重要核心功能。“认证”,是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点说就是系统认为用户是否能登录。“授权”指确定一个主体是否允许在你的应用程序执行一个动作的过程。通俗点讲就是系统判断用户是否有权限去做某些事情

<2>.SpringSecurity快速入门

导入依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.5.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>org.example</groupId>
	<artifactId>springsecuritydemo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>springsecuritydemo</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<!--SpringSecurity组件-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<!--web组件-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!--test组件-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- mybatis 依赖 -->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.1</version>
		</dependency>
		<!-- mysql 数据库依赖 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.18</version>
		</dependency>
		<!--thymeleaf springsecurity5 依赖-->
		<dependency>
			<groupId>org.thymeleaf.extras</groupId>
			<artifactId>thymeleaf-extras-springsecurity5</artifactId>
		</dependency>
		<!--thymeleaf依赖-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

导入前端页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input type="text" name="username123" /><br/>
    密码:<input type="password" name="password123" /><br/>
    记住我:<input type="checkbox" name="remember-me" value="true" /><br />
    <input type="submit" value="登录" />
</form>
</body>
</html>

访问页面:
导入spring-boot-starter-security 启动器后,Spring Security 已经生效,默认拦截全部请求,如果用户没有登录,跳转到内置登录页面。
在这里插入图片描述
默认的username为user,密码打印在控制台上,在浏览器中输入账号和密码后会显示 login.html 页面内容。
在这里插入图片描述

2.SpringSecurity基本原理

<1>.UserDetailsService详解

当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。如果需要自定义逻辑时,只需要实现UserDetailsService 接口即可。

public interface UserDetailsService {
	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

返回值是一个UserDetails接口:

public interface UserDetails extends Serializable {
	// ~ Methods
	Collection<? extends GrantedAuthority> getAuthorities();//获取所有权限
	String getPassword();//获取密码
	String getUsername();//获取用户名
	boolean isAccountNonExpired();//判断账号是否过期
	boolean isAccountNonLocked();//判断账号是否被锁定
	boolean isCredentialsNonExpired();//判断凭证(密码)是否过期
	boolean isEnabled();//是否可用
}

这里我们一般返回UserDetails的实现类User

<2>.PasswordEncoder 密码解析器详解

Spring Security 要求容器中必须有 PasswordEncoder 实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder 的bean对象。
接口介绍:

public interface PasswordEncoder {
	String encode(CharSequence rawPassword);//把参数按照特定的解析规则进行解析。
	//:验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;
如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个参数表示存储的密码。
	boolean matches(CharSequence rawPassword, String encodedPassword);
	//如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回 false。默认返回 false
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}
}

BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器
代码示例:

	@Test
	public void contextLoads() {
		PasswordEncoder pe = new BCryptPasswordEncoder();
		String encode = pe.encode("123");
		System.out.println(encode);
		boolean matches = pe.matches("1234", encode);
		System.out.println("========================");
		System.out.println(matches);
	}

3. SpringSecurity自定义登录逻辑及权限控制

<1>.自定义逻辑登录

当 进 行 自 定 义 登 录 逻 辑 时 需 要 用 到 之 前 讲 解 的 UserDetailsService 和 PasswordEncoder 。但是Spring Security 要求:当进行自定义登录逻辑时容器内必须有 PasswordEncoder 实例。所以不能直接 new 对象。
代码示例:
第一步:编写配置类,注入PasswordEncoder Bean:

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

第二步:编写自定义登录逻辑(账号密码需要从数据库获取,这里使用模拟的方法)

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

	@Autowired
	private PasswordEncoder pw;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

		System.out.println("执行了loadUserByUsername方法");

		//1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException异常
		if (!"admin".equals(username)){
			throw new UsernameNotFoundException("用户名不存在!");
		}
		//2.把查询出来的密码(注册时已经加密过)进行解析,或者直接把密码放入构造方法
		String password = pw.encode("123");
		//加入相应的权限
		return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
				"/main.html,/insert,/delete"));
	}
}

重启项目后,在浏览器中输入账号:admin,密码:123。后可以正确进入到 login.html 页面。

<2>.自定义登录页面

1.修改配置类
修改配置类中主要是设置哪个页面是登录页面。配置类需要继承WebSecurityConfigurerAdapte,并重写 configure方法。

successForwardUrl() //登录成功后跳转地址
loginPage() //登录页面
loginProcessingUrl //登录页面表单提交地址,此地址可以不真实存在。
antMatchers() //匹配内容
permitAll() //允许

<3>.自定义成功处理器

使用successForwardUrl()时表示成功后转发请求到地址。内部是通过 successHandler() 方法进行控制成功后交给哪个类进行处理

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());
		//输出null(保密)
		System.out.println(user.getPassword());
		System.out.println(user.getAuthorities());
		response.sendRedirect(url);
	}
}

<4>.自定义失败处理器

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);
	}
}

<5>.自定义登录配置代码示例

@Override
	protected void configure(HttpSecurity http) throws Exception {
		//表单提交
		http.formLogin()
				.usernameParameter("username123")
				.passwordParameter("password123")
				//当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
				.loginProcessingUrl("/login")
				//自定义登录页面
				.loginPage("/showLogin")
				//登录成功后跳转页面,Post请求
				.successForwardUrl("/toMain")
				//登录成功后处理器,不能和successForwardUrl共存
				// .successHandler(new MyAuthenticationSuccessHandler("/main.html"))
				//登录失败后跳转页面,Post请求
				.failureForwardUrl("/toError");
				//登录失败后处理器,不能和failureForwardUrl共存
				// .failureHandler(new MyAuthenticationFailureHandler("/error.html"));
}

4.访问控制及角色权限判断

<1>.访问控制URL匹配

在所有匹配规则中取所有规则的交集。配置顺序影响了之后授权效果,越是具体的应该放在前面,越是笼统的应该放到后面

(1).anyRequest()

在之前认证过程中我们就已经使用过 anyRequest(),表示匹配所有的请求。一般情况下此方法都会使用,设置全部内容都需要进行认证。

.anyRequest().authenticated();

(2).antMatcher()

public C antMatchers(String... antPatterns)

参数是不定向参数,每个参数是一个 ant 表达式,用于匹配 URL规则。
规则如下:
? : 匹配一个字符
*:匹配 0 个或多个字符
** :匹配 0 个或多个目录

(3).regexMatchers()

使用正则表达式进行匹配。和 antMatchers() 主要的区别就是参数, antMatchers() 参数是 ant 表达式,regexMatchers() 参数是正则表达式

<2>.内置访问控制方法

1.permitAll()
permitAll()表示所匹配的 URL 任何人都允许访问
2.authenticated()
authenticated()表示所匹配的 URL 都需要被认证才能访问。
3.anonymous()
anonymous()表示可以匿名访问匹配的URL。和permitAll()效果类似,只是设置为 anonymous()的 url 会执行 filter链中
4.denyAll()
denyAll()表示所匹配的5. URL 都不允许被访问。
rememberMe()
被“remember me”的用户允许访问
5.fullyAuthenticated()
如果用户不是被 remember me 的,才可以访问

<3>.角色权限判断

1.hasAuthority(String)
判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建 User 对象时指定的。 admin和normal 就是用户的权限。admin和normal 严格区分大小写

	return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
				"/main.html,/insert,/delete"));

在配置类中通过 hasAuthority(“admin”)设置具有 admin 权限时才能访问。

.antMatchers("/main1.html").hasAuthority("admin")

2.hasAnyAuthority(String …)
如果用户具备给定权限中某一个,就允许访问。下面代码中由于大小写和用户的权限不相同,所以用户无权访问

.antMatchers("/main1.html").hasAnyAuthority("adMin","admiN")

3.hasRole(String)
如果用户具备给定角色就允许访问。否则出现 403。
参数取值来源于自定义登录逻辑 UserDetailsService 实现类中创建 User 对象时给 User 赋予的授权。
在给用户赋予角色时角色需要以: ROLE_开头 ,后面添加角色名称。例如:ROLE_abc 其中 abc 是角色名,ROLE_是固定的字符开头。
使用 hasRole()时参数也只写 abc 即可。否则启动报错。

return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc," +
				"/main.html,/insert,/delete"));
.antMatchers("/main1.html").hasRole("abc")

4.hasAnyRole(String …)
如果用户具备给定角色的任意一个,就允许被访问

5.hasIpAddress(String)
如果请求是指定的 IP 就运行访问。
可以通过 request.getRemoteAddr() 获取 ip 地址。
需要注意的是在本机进行测试时 localhost 和 127.0.0.1 输出的 ip地址是不一样的。
当浏览器中通过 localhost 进行访问时控制台打印的内容:0:0:0:0:0:0:0:1

角色权限判断代码示例:


		//授权认证
		http.authorizeRequests()
				//error.html不需要被认证
				// .antMatchers("/error.html").permitAll()
				.antMatchers("/error.html").access("permitAll()")
				//login.html不需要被认证
				// .antMatchers("/login.html").permitAll()
				.antMatchers("/showLogin").access("permitAll()")
				.ant

以上是关于轻松上手SpringSecurity,OAuth,JWT的主要内容,如果未能解决你的问题,请参考以下文章

SpringSecurity-OAuth2万文详解

SpringSecurity——OAuth2框架鉴权实现源码分析

使用SpringSecurity体验OAuth2 (入门2)

自定义springsecurity OAuth 2错误输出问题,怎么解决

spring-security-oauth2.0 SSO大体流程图

自定义springsecurity OAuth 2错误输出问题,怎么解决