SpringSecurity
1.学习目标
- SpringSecurity
- Oauth2
- SpringSecurity-Oauth2
- JWT
- SpringSecurityOauth2整合JWT
- SpringSecurityOauth2整合SSO
2.SpringSecurity简介
2.1.1安全框架概述
什么是安全框架?解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制,非常麻烦。使用安全框架,我们可以通过配置的方式实现对资源的访问限制。
2.1.2常用的安全框架
- SpringSecurity:Spring家族的一员,是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring上下文中配置的bean,充分利用了Spring的IOC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
- Apache Shiro:一个功能强大且易于使用的java安全框架,提供了认证授权,加密和会话管理。
2.1.3SpringSecurity简介
概述
SpringSecurity是一个高度自定义的安全框架,利用Spring IoC/DI
和AOP
功能,为系统提供了声明式安全访问控制功能,减少了为系统安全而编写大量重复代码的工作。使用Spring Security的原因有很多,但大部分都是发现了JAVAEE的Servlet规范或EJB规范中的安全功能缺乏典型应用场景。同时认识到他们在WAR或EAR级别无法移植。因此,如果你更换服务器环境,还有大量工作去重新配置你的应用程序。使用SpringSecurity解决了这些问题,也为你提供了其他有用的,可定制的安全功能,正如你可能知道的两个应用程序的两个主要区域是 ”认证“ 和 ”授权“ (或者访问控制)。这两点也是Spring Security重要核心功能。
- 认证:是建立一个声明的主体过程(一个主体一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统),通俗点来说就是系统判断用户是否能登录。
- 授权:判断用户是否有权去做某事。。
2.2快速入门
2.2.1导入依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>SecurityStudye</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.2.2.RELEASE</version>
</parent>
<dependencies>
<!--spring security组件-->
<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>
</dependencies>
</project>
编写基本控制器
package com.yealike.security.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class LoginController {
@RequestMapping("/login")
public String login(){
System.out.println("执行了登录方法!");
return "redirect:main.html";
}
}
编写登录页面
登录页面中用户名和密码的参数必须为username和password,后面解释
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
<style>
.box1{
border: brown 1px solid;
width: 600px;
height: 300px;
margin: 0 auto;
}
h1{
text-align: center;
color: brown;
font-size: 36px;
}
</style>
</head>
<body>
<div class="box1">
<h1>登录页面</h1>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密 码:<input type="text" name="password"><br>
<button style="margin-left: 100px;margin-top: 20px" type="submit" value="登录">登录</button>
</form>
</div>
</body>
</html>
登录成功跳转页面
我暂且把这叫做主页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录成功</title>
<style>
h1 {
color: black;
font-size: 45px;
text-align: center;
}
</style>
</head>
<body>
<h1>登录成功!</h1>
</body>
</html>
目录结构
点击运行后
在浏览器中使用localhost:8080/login.html
访问我们的资源
控制台会显示如下信息
Using generated security password: 64acbd7a-725c-4be3-986f-7c7e4fd3fdc0
这就是SpringSecurity给我们生成的默认密码,此外还会生成一个默认的登录页面
默认用户名:user
密码:64acbd7a-725c-4be3-986f-7c7e4fd3fdc0(就是控制台输出的默认密码)
登录之后才可以访问我们想要访问的页面,首先访问login.html输入用户名密码提交过后就会跳转到main.html页面。
2.2.2UserDetailsService详解
看看源码,源码中只有一个方法,根据用户名加载用户–返回UserDetails类。
- 这个用户名就是前端返回过来的用户名
- 通过前端传入的用户名查找数据库,如果数据库不存在该用户名则抛异常
throws UsernameNotFoundException;
public interface UserDetailsService {
// ~ Methods
//
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
查看UserDetails类中方法。
public interface UserDetails extends Serializable {
// ~ Methods
/**
* Returns the authorities granted to the user. Cannot return <code>null</code>.
*
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* Returns the password used to authenticate the user.
*
* @return the password
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return <code>null</code>.
*
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
*判断用户是否过期
* @return <code>true</code> if the user's account is valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
*
* @return <code>true</code> if the user is not locked, <code>false</code> otherwise
*/
boolean isAccountNonLocked();
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
*用户凭证(密码)是否过期
* @return <code>true</code> if the user's credentials are valid (ie non-expired),
* <code>false</code> if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
*
* @return <code>true</code> if the user is enabled, <code>false</code> otherwise
*/
boolean isEnabled();
}
UserDetails是一个接口,这个接口规定了一些方法。
SpringSecurity提供了这个接口的实现类User,注意这个实现类,不要和我们定义的User类混淆
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private static final Log logger = LogFactory.getLog(User.class);
// ~ Instance fields
// ==================================================
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
// ~ Constructors
// ==================================================
/**
* Calls the more complex constructor with all boolean arguments set to {@code true}.
*/
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException(
"Cannot pass null or empty values to constructor");
}
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
// ~ Methods
// ====================================================
public Collection<GrantedAuthority> getAuthorities() {
return authorities;
}
public String getPassword() {
return password;
}
public String getUsername() {
return username;
}
public boolean isEnabled() {
return enabled;
}
public boolean isAccountNonExpired() {
return accountNonExpired;
}
public boolean isAccountNonLocked() {
return accountNonLocked;
}
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
public void eraseCredentials() {
password = null;
}
private static SortedSet<GrantedAuthority> sortAuthorities(
Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
// Ensure array iteration order is predictable (as per
// UserDetails.getAuthorities() contract and SEC-717)
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
new AuthorityComparator());
for (GrantedAuthority grantedAuthority : authorities) {
Assert.notNull(grantedAuthority,
"GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
private static class AuthorityComparator implements Comparator<GrantedAuthority>,
Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
public int compare(GrantedAuthority g1, GrantedAuthority g2) {
// Neither should ever be null as each entry is checked before adding it to
// the set.
// If the authority is null, it is a custom authority and should precede
// others.
if (g2.getAuthority() == null) {
return -1;
}
if (g1.getAuthority() == null) {
return 1;
}
return g1.getAuthority().compareTo(g2.getAuthority());
}
}
/**
* Returns {@code true} if the supplied object is a {@code User} instance with the
* same {@code username} value.
* <p>
* In other words, the objects are equal if they have the same username, representing
* the same principal.
*/
@Override
public boolean equals(Object rhs) {
if (rhs instanceof User) {
return username.equals(((User) rhs).username);
}
return false;
}
/**
* Returns the hashcode of the {@code username}.
*/
@Override
public int hashCode() {
return username.hashCode();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(super.toString()).append(": ");
sb.append("Username: ").append(this.username).append("; ");
sb.append("Password: [PROTECTED]; ");
sb.append("Enabled: ").append(this.enabled).append("; ");
sb.append("AccountNonExpired: ").append(this.accountNonExpired).append("; ");
sb.append("credentialsNonExpired: ").append(this.credentialsNonExpired)
.append("; ");
sb.append("AccountNonLocked: ").append(this.accountNonLocked).append("; ");
if (!authorities.isEmpty()) {
sb.append("Granted Authorities: ");
boolean first = true;
for (GrantedAuthority auth : authorities) {
if (!first) {
sb.append(",");
}
first = false;
sb.append(auth);
}
}
else {
sb.append("Not granted any authorities");
}
return sb.toString();
}
/**
* Creates a UserBuilder with a specified user name
*
* @param username the username to use
* @return the UserBuilder
*/
public static UserBuilder withUsername(String username) {
return builder().username(username);
}
/**
* Creates a UserBuilder
*
* @return the UserBuilder
*/
public static UserBuilder builder() {
return new UserBuilder();
}
@Deprecated
public static UserBuilder withDefaultPasswordEncoder() {
logger.warn("User.withDefaultPasswordEncoder() is considered unsafe for production and is only intended for sample applications.");
PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
return builder().passwordEncoder(encoder::encode);
}
public static UserBuilder withUserDetails(UserDetails userDetails) {
return withUsername(userDetails.getUsername())
.password(userDetails.getPassword())
.accountExpired(!userDetails.isAccountNonExpired())
.accountLocked(!userDetails.isAccountNonLocked())
.authorities(userDetails.getAuthorities())
.credentialsExpired(!userDetails.isCredentialsNonExpired())
.disabled(!userDetails.isEnabled());
}
/**
* Builds the user to be added. At minimum the username, password, and authorities
* should provided. The remaining attributes have reasonable defaults.
*/
public static class UserBuilder {
private String username;
private String password;
private List<GrantedAuthority> authorities;
private boolean accountExpired;
private boolean accountLocked;
private boolean credentialsExpired;
private boolean disabled;
private Function<String, String> passwordEncoder = password -> password;
/**
* Creates a new instance
*/
private UserBuilder() {
}
/**
* Populates the username. This attribute is required.
*
* @param username the username. Cannot be null.
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder username(String username) {
Assert.notNull(username, "username cannot be null");
this.username = username;
return this;
}
/**
* Populates the password. This attribute is required.
*
* @param password the password. Cannot be null.
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder password(String password) {
Assert.notNull(password, "password cannot be null");
this.password = password;
return this;
}
/**
* Encodes the current password (if non-null) and any future passwords supplied
* to {@link #password(String)}.
*
* @param encoder the encoder to use
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder passwordEncoder(Function<String, String> encoder) {
Assert.notNull(encoder, "encoder cannot be null");
this.passwordEncoder = encoder;
return this;
}
/**
* Populates the roles. This method is a shortcut for calling
* {@link #authorities(String...)}, but automatically prefixes each entry with
* "ROLE_". This means the following:
*
* <code>
* builder.roles("USER","ADMIN");
* </code>
*
* is equivalent to
*
* <code>
* builder.authorities("ROLE_USER","ROLE_ADMIN");
* </code>
*
* <p>
* This attribute is required, but can also be populated with
* {@link #authorities(String...)}.
* </p>
*
* @param roles the roles for this user (i.e. USER, ADMIN, etc). Cannot be null,
* contain null values or start with "ROLE_"
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder roles(String... roles) {
List<GrantedAuthority> authorities = new ArrayList<>(
roles.length);
for (String role : roles) {
Assert.isTrue(!role.startsWith("ROLE_"), () -> role
+ " cannot start with ROLE_ (it is automatically added)");
authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
}
return authorities(authorities);
}
/**
* Populates the authorities. This attribute is required.
*
* @param authorities the authorities for this user. Cannot be null, or contain
* null values
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
* @see #roles(String...)
*/
public UserBuilder authorities(GrantedAuthority... authorities) {
return authorities(Arrays.asList(authorities));
}
/**
* Populates the authorities. This attribute is required.
*
* @param authorities the authorities for this user. Cannot be null, or contain
* null values
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
* @see #roles(String...)
*/
public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList<>(authorities);
return this;
}
/**
* Populates the authorities. This attribute is required.
*
* @param authorities the authorities for this user (i.e. ROLE_USER, ROLE_ADMIN,
* etc). Cannot be null, or contain null values
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
* @see #roles(String...)
*/
public UserBuilder authorities(String... authorities) {
return authorities(AuthorityUtils.createAuthorityList(authorities));
}
/**
* Defines if the account is expired or not. Default is false.
*
* @param accountExpired true if the account is expired, false otherwise
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder accountExpired(boolean accountExpired) {
this.accountExpired = accountExpired;
return this;
}
/**
* Defines if the account is locked or not. Default is false.
*
* @param accountLocked true if the account is locked, false otherwise
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder accountLocked(boolean accountLocked) {
this.accountLocked = accountLocked;
return this;
}
/**
* Defines if the credentials are expired or not. Default is false.
*
* @param credentialsExpired true if the credentials are expired, false otherwise
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder credentialsExpired(boolean credentialsExpired) {
this.credentialsExpired = credentialsExpired;
return this;
}
/**
* Defines if the account is disabled or not. Default is false.
*
* @param disabled true if the account is disabled, false otherwise
* @return the {@link UserBuilder} for method chaining (i.e. to populate
* additional attributes for this user)
*/
public UserBuilder disabled(boolean disabled) {
this.disabled = disabled;
return this;
}
public UserDetails build() {
String encodedPassword = this.passwordEncoder.apply(password);
return new User(username, encodedPassword, !disabled, !accountExpired,
!credentialsExpired, !accountLocked, authorities);
}
}
}
注意这里面的password不是前端输入过来的密码,而是数据库里面存的密码
前端输入用户名和密码之后,SpringSecurity通过用户名去查询数据库
根据用户名查出数据库的密码,与前端密码相比较是否相同
public User(String username, String password,
Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
2.2.3PasswordEncoder详解
- PasswordEncoder也是一个接口
- 当我们什么都没有配置的时候,Spring Security已经默认实现了这个接口
- 我们自定义验证逻辑的时候,就要手动实现这个接口
- 这个接口一共有三个方法
public interface PasswordEncoder {
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*对密码进行加密的方法,传入原始密码,返回加密后的密码,这个方法是单向的,只能用来加密,不能用来解密
*/
String encode(CharSequence rawPassword);
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
*判断加密后的密码和原始密码是否一致
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
这个接口有众多实现类,官方推荐使用BCryptPasswordEncoder
编写测试方法
@SpringBootTest
public class SecurityTest {
@Test
public void passwordEncoder() {
PasswordEncoder pe = new BCryptPasswordEncoder();
String encode = pe.encode("123");
System.out.println("加密后的密码===>" + encode);
boolean matches = pe.matches("123", encode);
System.out.println("原始密码与加密后是否相同===>"+matches);
}
}
控制台输出
加密后的密码===>$2a 10 10 10UXFsphkbvhcNJfIc8S5vDOdXDShDG769JtAhRPh0gbwSmABTrZuay
原始密码与加密后是否相同===>true
总结:PasswordEncoder的作用是加密原密码,判断原密码与加密后的密码是否相同。
2.2.4自定义登录逻辑
- 我们在进行自定义登录逻辑时:容易内必须有PasswordEncoder的实例,所以我们不能像在测试的时候直接通过new的方式获取实例。
- 我们通过编写配置类的方法,将PasswordEncoder注入Spring容器。
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
- 有了这个实例之后,我们去编写UserDetailsService的接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException
// 我们模拟查询数据库的过程,假设用户名就为 admin
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在!");
}
// 2.把查询出来的密码(注册时就已经加密后存入数据库的)进行解析,或者直接把密码放入构造方法
String password = pw.encode("123");
// 赋予当前用户admin,normal权限
return new User(username,password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
- 现在运行程序,发现控制台不输出用户名和密码了。
- 用户名和密码变成了我们设置的admin和123
- 到页面输入我们设置的用户名和密码,登录成功
2.2.5自定义登录页面
- 我们前面已经实现了自定义的登录逻辑
- 但是登录页面还是SpringSecurity提供的登录页面。
- 我们自己写的登录页面,丝毫没有排上用场。
- 我们可以在我们之前写的配置类里面进行相应的配置来实现自定义登录页面。
实现自定义登录页面
- 配置类继承
WebSecurityConfigurerAdapter
类 - 实现参数为
HttpSecurity
的方法configure
IDEA快捷键Ctrl+O
- 配置类详细配置如下
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 自定义登录页面
.loginPage("/login.html");
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
- 浏览器访问 http://localhost:8080/login.html
- 直接跳转到了我们自定义的登录页面
- 很丑
这个时候有一点需要注意。
我们在浏览器输入http://localhost:8080/main.html访问主页面的时候,也是直接跳转,不需要登录。
这么干不符合我们的业务逻辑,登录页面就是摆设,不但丑,还毫无用处。
我们需要通过授权控制指定页面放行与拦截的方式来解决这种问题。
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 自定义登录页面
.loginPage("/login.html");
// 授权认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
}
这个时候再访问main.html就会自动跳转到登录页面。
但是新的问题又出现了,我们输入了正确的用户名和密码之后,页面不发生跳转
说明只是登录页面换成了我们自己的页面,登录逻辑并没有衔接上SpringSecurity的登录逻辑
- 配置登录地址
.loginProcessingUrl("/login")
- 配置登录成功之后调换页面
.successForwardUrl("/main.html")
- 配置上一步后登录报405错误,请求方式不被允许因为默认是get请求
- 配置跳转处理方法
.successForwardUrl("/toMain");
在控制器中配置该方法通过重定向的方式跳转到主页面
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
.successForwardUrl("/toMain");
// 授权认证
http.authorizeRequests()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
// 关闭csrf防火墙
http.csrf().disable();
}
在controller里面新建方法toMain
@RequestMapping("/toMain")
public String toMain(){
System.out.println("执行了登录方法!");
return "redirect:main.html";
}
这个时候,正确输入用户名和密码之后,页面成功跳转
2.2.6失败跳转
配置登录失败的页面。
- 编写登录失败页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录失败</title>
<style>
h1 {
text-align: center;
font-size: 66px;
color: blue;
}
a {
font-size: 40px;
color: darkblue;
}
</style>
</head>
<body>
<h1>登录失败。。。。。。。。。</h1><br>
<a href="/login.html">重新登录</a>
</body>
</html>
2.配置类配置失败跳转路径与拦截规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
.successForwardUrl("/toMain")
.failureForwardUrl("/toError");
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").permitAll()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
// 关闭csrf防火墙
http.csrf().disable();
}
3.控制器编写跳转逻辑
@RequestMapping("/toError")
public String toError(){
return "redirect:error.html";
}
重新登录后输入错误的用户名或密码之后,就会跳转到登录失败的页面
2.2.7设置请求账户和密码的参数名
- 在之前编写的登录表单里面
- 用户名和密码的参数名称必须叫做username和password请求方式必须为post
- 这是因为在过滤器中,已经规定了他们的名称,不能乱来
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密 码:<input type="text" name="password"><br>
<button style="margin-left: 100px;margin-top: 20px" type="submit" value="登录">登录</button>
</form>
UsernamePasswordAuthenticationFilter
源码如下
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =======================================================================
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
设置自定义的登录用户名和密码
在配置类中配置
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 自定义登录参数
.usernameParameter("hello")
.passwordParameter("world")
// 当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
.successForwardUrl("/toMain")
.failureForwardUrl("/toError");
这时候我们就可以将用户名的参数设置为hello,密码参数为world
<form action="/login" method="post">
用户名:<input type="text" name="hello"><br>
密 码:<input type="text" name="world"><br>
<button style="margin-left: 100px;margin-top: 20px" type="submit" value="登录">登录</button>
</form>
运行程序:实测,可以正常登录
2.2.8自定义登录成功处理器
- 关于登录成功之后的跳转路径方法
- 上述方法挺好用的
.successForwardUrl("/toMain")
- 缺点就是,不适合应用于前后端分离的项目,无法进行站外挑战。
- 分析源码,实现自定义的处理规则需要实现
AuthenticationSuccessHandler
接口
编写自定义的登录成功处理器
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url) {
this.url = url;
}
/**
* Called when a user has been successfully authenticated.
*
* @param request the request which caused the successful authentication
* @param response the response
* @param authentication the <tt>Authentication</tt> object which was created during
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.sendRedirect(url);
}
}
重新编写跳转规则
successHandler
:自定义跳转处理不能和successForwardUrl
共存
http.formLogin()
// 自定义登录参数
// .usernameParameter("hello")
// .passwordParameter("world")
// 当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
// .successForwardUrl("/toMain")
// 登录成功后处理器,不能和successForwardUrl共存
.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
.failureForwardUrl("/toError");
重启项目:登录成功之后,就跳转到了百度的首页
-
在
onAuthenticationSuccess
方法中参数Authentication
可以得到我们当前登录用户对象 -
出于安全考虑,不会输出密码。密码会输出ull
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
String password = user.getPassword();
Collection<GrantedAuthority> authorities = user.getAuthorities();
System.out.println("用户名===>"+username);
System.out.println("密码===>"+password);
System.out.println("权限===>"+authorities);
response.sendRedirect(url);
}
再次输入用户名密码之后,控制台输出
用户名===>admin
密码===>null
权限===>[admin, normal]
2.2.9自定义登录失败处理器
- 有登录成功处理器,就有对应的登录失败处理器。
编写处理器
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
private String url;
public MyAuthenticationFailureHandler(String url) {
this.url = url;
}
/**
* Called when an authentication attempt fails.
*
* @param request the request during which the authentication attempt occurred.
* @param response the response.
* @param exception the exception which was thrown to reject the authentication
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect(url);
}
}
调整跳转规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
// 当发现/login时认为是登录,必须和表单提交的地址一样,去执行UserDetailsServiceImpl
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
// .successForwardUrl("/toMain")
// 登录成功后处理器,不能和successForwardUrl共存
.successHandler(new MyAuthenticationSuccessHandler("http://www.baidu.com"))
// .failureForwardUrl("/toError")
// 登录失败的跳转页面
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
执行程序,输入错误用户名或密码
实测,跳转error.html页面成功
2.2.10 anyRequest
- 在认证授权中,antMatchers相当于单独认证。
- anyRequest:具有查漏补缺的作用,将其余的未作授权认证的做统一处理
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").permitAll()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
2.2.11 antMachers()
方法定义如下
public C antMatchers(String... antPatterns)
参数是不定向参数
- ?:匹配一个字符
- *:匹配0个或多个字符
- **:匹配0个或多个目录
在实际项目中经常需要放行所有静态资源,下面演示放行js文件夹下所有脚本文件。
.antMachers("/js/**","css/**").permitAll()
还有一种方式是只要是.js文件都放行
.antMachers("/**/*.js").permitAll()
- regexMatchers:通过正则表达式来判断是否放行某些资源
2.3进阶
2.3.1内置访问控制方法介绍
- 授权认证匹配的公式就是
- 前面是匹配的url
antMatchers("/error.html")
或者.anyRequest()
- 后面是权限访问的控制
permitAll()
- 接下来,我们扒一下源码,看看SpringSecurity一共有几种认证方式。
public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
extends
AbstractInterceptUrlConfigurer<ExpressionUrlAuthorizationConfigurer<H>, H> {
static final String permitAll = "permitAll";
private static final String denyAll = "denyAll";
private static final String anonymous = "anonymous";
private static final String authenticated = "authenticated";
private static final String fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe";
从源码中我们可以看出,SpringSecurity一共定义了6中访问控制方法。
permitAll
:允许任何人(url)访问denyAll
:所有的url都不允许被访问anonymous
:可以匿名访问authenticated
:所有的url需要进行相应的认证才能进行访问fullyAuthenticated
:需要完全认证rememberMe
:记住我
2.3.2权限判断
除了之前讲解的内置权限控制。Spring Security中还支持很多权限控制,这些方法一般都用于用户已经被认证后,判断用户是否具有特定的要求。
- 判断用户是否有指定的身份,比如会员身份,判断完成之后给与用户不同的访问资源的权限
- 前面d额内置访问控制方法是在用户登录之前判断的
- 角色权限判断是在用户登录之后判断的
判断用户是否具有特定的权限,用户的权限是在自定义登录逻辑中创建User对象时指定的,下图中admin和normal就是用户的权限,admin和normal严格区分大小写。
// 赋予当前用户admin,normal权限
return new User(username,password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
权限测试
在main.html里面添加超链接,跳转至另一个页面
<h1>登录成功!</h1><br>
<a href="./main1.html">跳转</a>
创建main1.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>这里是main1.html</h1>
</body>
</html>
修改授权规则
- 在授权规则中添加此配置
.antMatchers("/main1.html").hasAuthority("admin")
- 表示只有具有
admin
权限才能访问main1.html
注意权限匹配时admin严格区分大小写。
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").permitAll()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/main1.html").hasAuthority("admin")
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated()
将url匹配方式修改为简单的匹配模式
http.formLogin()
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
.successForwardUrl("/toMain")
// 登录失败的跳转页面
.failureHandler(new MyAuthenticationFailureHandler("/error.html"));
-
启动程序,登录成功之后,进入主页面,点击跳转
-
由于我们的当前用户已经具有了admin权限,所有正常跳转到了main1.html页面
-
当我们修改权限,将admin改为大写Admin的时候
-
再次跳转时,失败,页面信息如下报403
Sat Apr 02 16:37:07 CST 2022
There was an unexpected error (type=Forbidden, status=403).
Forbidden
- 现在我们进行的权限匹配模式是匹配单个权限
- 如果匹配多个用户权限方法是
hasAnyAuthority("Admin","normal")
- 比匹配单个权限多了一个any
hasAnyAuthority("Admin","normal")
表示存在所列权限中的一个就可以访问指定路径- 启动程序测试,跳转成功
- hasAuthority(“admin”):匹配当个权限
- hasAnyAuthority(“Admin”,“normal”):匹配多个权限
2.3.2 角色判断
- 如果登录用户包含某些角色,则允许访问
- 否则报403
- 我们可以在自定义登录逻辑中添加用户角色
- 用户角色与用户权限一起定义
- 角色以
ROLE_
开头否则SpringSecurity不识别
在UserDetailsServiceImpl中编写
// 赋予当前用户admin,normal权限
return new User(username,password,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc"));
在SecurityConfig中进行授权
antMatchers("/main1.html").hasRole("abc")
授权认证的时候不需要加 ROLE_
前缀
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").permitAll()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/main1.html").hasRole("abc")
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
扒源码,里面规定了,添加权限要ROLE_
private static String hasAnyRole(String... authorities) {
String anyAuthorities = StringUtils.arrayToDelimitedString(authorities,
"','ROLE_");
return "hasAnyRole('ROLE_" + anyAuthorities + "')";
}
启动程序,测试成功
与权限判断相似
hasRole("abc")
:判断一个角色
hasAnyRole("abc")
:判断多个角色
2.3.3 IP地址判断
应用场景:现在有一个后台管理系统,这个后台管理系统只允许指定的服务器访问,那么只要将该服务器的IP地址配置进授权认证中就可以访问该后台管理系统了,其他的ip是无法访问该系统的。
配置登录规则
http.formLogin()
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
.failureForwardUrl("/toError");
在MyAuthenticationSuccessHandler中获取本机ip地址
request.getRemoteAddr()
获取本机IP
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("本机ip地址===>"+request.getRemoteAddr());
User user = (User) authentication.getPrincipal();
String username = user.getUsername();
String password = user.getPassword();
Collection<GrantedAuthority> authorities = user.getAuthorities();
System.out.println("用户名===>"+username);
System.out.println("密码===>"+password);
System.out.println("权限===>"+authorities);
response.sendRedirect(url);
}
启动程序,使用localhost访问,得出地址0:0:0:0:0:0:0:1
使用127.0.0.1得出的也是127.0.0.1
使用192.168.0.107得出也是192.168.0.107
用户已经登录。。。
本机ip地址===>0:0:0:0:0:0:0:1
用户名===>admin
密码===>null
权限===>[ROLE_abc, admin, normal]
配置ip匹配规则
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
这时候再使用192.168.0.107登录的时候就无法访问main1.html资源
2.3.4 自定义403处理方案
- 我们在前面的测试中经常会出现403错误的大白板
- 403的意思是无权限
- 这个大白板对于用户来说是十分不友好的
实现自定义403处理方案一共有两步
第一步:创建一个类 MyAccessDeniedHandler
实现AccessDeniedHandler
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
/**
* Handles an access denied failure.
*
* @param request that resulted in an <code>AccessDeniedException</code>
* @param response so that the user agent can be advised of the failure
* @param accessDeniedException that caused the invocation
* @throws IOException in the event of an IOException
* @throws ServletException in the event of a ServletException
*/
@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();
}
}
第二步 在配置类引用
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
编写第三个http方法
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
配置类完整代码
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录成功后处理器,不能和successForwardUrl共存
.successHandler(new MyAuthenticationSuccessHandler("main.html"))
.failureForwardUrl("/toError");
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").permitAll()
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.antMatchers("/main1.html").hasIpAddress("127.0.0.1")
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
// 关闭csrf防火墙
http.csrf().disable();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
}
- 我们刚刚测试的是指定的IP地址,我们配置的是
127.0.0.1
- 我使用本机IP
192.168.0.10
访问资源 - 当进行页面跳转的时候,出现了如下页面
2.3.5 基于表达式的访问控制
1.access()方法使用
- 之前学习的登录用户权限判断实际上底层都是调用access(表达式)
- 所以我们可以通过使用access()表达式来实现以前的功能
- 改造原来的授权认证
.antMatchers("/error.html").access("permitAll()")
//.antMatchers("/error.html").permitAll()
与注释掉的功能一样。感觉还不如不改造
2.access结合自定义方法实现权限控制
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
-
上面的方法的作用是所有的请求都必须被认证。
-
我们可以做一个自定义的方法实现权限控制
-
判断当前用户是否有访问某些url的权限,如果有,才可以访问
编写接口
public interface MyService {
/**
* 判断当前用户是否有访问某些资源的权限
*
* @param request 获得主体和对应的权限
* @param authentication 权限
* @return 是否可以访问资源
*/
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
编写实现类
@Service
public class MyServiceImpl implements MyService {
/**
* 判断当前用户是否有访问某些资源的权限
*
* @param request 获得主体和对应的权限
* @param authentication 权限
* @return 是否可以访问资源
*/
@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;
}
}
修改配置类相关信息
.anyRequest().access("@myServiceImpl.hasPermission(request,authentication)");
//.anyRequest().authenticated();
这个时候启动程序,进行正常访问
结果出现403页面
出现权限不足的原因是,在UserDeatilsService里面没有配置可访问的url
因为我们写的这个方法是自定义方法实现权限控制
判断当前用户是否有访问某些url的路径。
我们需要在UserDetailsServiceImpl
添加可访问的url
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder pw;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("用户已经登录。。。");
// 1.查询数据库判断用户名是否存在,如果不存在就会抛出UsernameNotFoundException
// 我们模拟查询数据库的过程,假设用户名就为 admin
if (!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在!");
}
// 2.把查询出来的密码(注册时就已经加密后存入数据库的)进行解析,或者直接把密码放入构造方法
String password = pw.encode("123");
// 赋予当前用户admin,normal权限
return new User(username,password,
AuthorityUtils
.commaSeparatedStringToAuthorityList("admin,normal,ROLE_abc,/main.html"));
}
}
在最后添加上/main.html
重启项目进行测试,正确输入用户名和密码之后,就进行了正常的页面跳转
实测,登录之后成功进入首页
2.3.6基于注解的访问控制
在Spring Security中提供了一些访问控制的注解,这些注解都是默认不可用的,需要通过@EnableGlobalMethodSecurity
注解进行开启后使用。(可以将注解放到controller或service上)
如果设置的条件允许,程序正常执行,如果不允许会报500
这个注解可以放到Service接口或者controller的方法上面。通常情况下都是写在控制器方法上,控制接口url是否被允许访问。
1.@Secured
@Secured
是专门用于判断是否具有角色的,能写在方法或类上,参数需要以ROLE_开头。
测试
修改配置类信息
不需要进行角色判断,取消自定义的权限控制
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
.successForwardUrl("/toMain")
.failureForwardUrl("/toError");
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").access("permitAll()")
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
// 所有请求都必须被认证,必须登录之后才能被访问
.anyRequest().authenticated();
// 关闭csrf防火墙
http.csrf().disable();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
}
测试注解
在启动类上添加注解,确认启用true
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
在登录控制器的方法上添加注解,这时候的角色判断必须带ROLE_前缀
与.antMatchers("/main1.html").hasRole("abc")
@Secured("ROLE_abc")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:main.html";
}
2.@PreAuthorize()
、@PostAuthorize()
- 这两个注解都是方法或类级别的注解
@PreAuthorize()
表示访问方法或类在执行之前先判断权限,大多数情况下都是使用这个注解,注解的参数和access()
方法参数相同,都是权限表达式。@PostAuthorize()
表示方法或类执行结束后判断权限,此注解很少使用。
启用注解,在启动类上添加属性prePostEnabled = true
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class, args);
}
}
再次判断角色,里面的参数和access()表达式一样
// @Secured("ROLE_abc")
@PreAuthorize("hasRole('abc')")
@RequestMapping("/toMain")
public String toMain(){
return "redirect:main.html";
}
- 注解
@PreAuthorize("hasRole('abc')")
有比配置类更加强大的地方 - 不管是不是以
ROLE_
开头都没有影响,但是也遵循角色大小写的区分 - 配置类不允许ROLE_开头
2.3.7 RememberMe功能实现
SpringSecurity中RemeberMe为记住我功能,用户只需要在登录时添加remember-me复选框,取值为true,SpringSecurity会自动把用户信息存储到数据源中,以后就可以不登录进行访问。
1.添加依赖
SpringSecurity实现RememberMe功能时,底层实现依赖Spring-JDBC,所以需要导入Spring-JDBC。以后多使用MyBatis框架而很少实现spring-jdbc,所以此处导入mybatis启动器同时还需要添加MySQL驱动。
<!--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>
</dependency>
2.配置数据源
在application.properties中配置数据源,确保数据库中有数据
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/security?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456
创建数据库
create database security;
use security;
编写记住我的配置类
- 编写新的http方法
- 将
PersistentTokenRepository
添加到Spring容器并注入 - 注入数据源
DataSource
(java.sql)和UserDetailsService
- 自动建表,第一次用过之后要及时注释掉
jdbcTokenRepository.setCreateTableOnStartup(true);
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAccessDeniedHandler myAccessDeniedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单提交
http.formLogin()
.loginProcessingUrl("/login")
// 自定义登录页面
.loginPage("/login.html")
// 登录之后跳转的页面,必须是post请求
.successForwardUrl("/toMain")
.failureForwardUrl("/toError");
// 授权认证
http.authorizeRequests()
// error.html不需要被认证
.antMatchers("/error.html").access("permitAll()")
// login.html不需要被认证
.antMatchers("/login.html").permitAll()
.anyRequest().authenticated();
// 关闭csrf防火墙
http.csrf().disable();
// 异常处理
http.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandler);
//记住我
http.rememberMe()
//自定义登录逻辑
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
}
@Bean
public PasswordEncoder getPw(){
return new BCryptPasswordEncoder();
}
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// 自动建表,第一次启动的时候需要建表,第二次启动的时候需要注释掉
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
去登录页面。编写记住我复选框
注意:记住我的参数必须是remember-me
<h1>登录页面</h1>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密 码:<input type="text" name="password"><br>
记住我:<input type="checkbox" name="remember-me" value="true"><br>
<button style="margin-left: 100px;margin-top: 20px" type="submit" value="登录">登录</button>
</form>
启动项目后自动建表
表中包含四个字段
用户名,序列号(主键)token和最近登录时间
在浏览器输入用户名和密码之后就会生成响应的数据
再此重新启动项目,启动前注释建表配置
// 自动建表,第一次启动的时候需要建表,第二次启动的时候需要注释掉
// jdbcTokenRepository.setCreateTableOnStartup(true);
在浏览器输入http://localhost:8080/main.html直接就能跳转到指定页面,不需要再登录
这个的默认失效时间是2周
自定义修改失效时间.tokenValiditySeconds(60)
单位秒
//记住我
http.rememberMe()
// 默认失效时间
.tokenValiditySeconds(60)
//自定义登录逻辑
.userDetailsService(userDetailsService)
// 持久层对象
.tokenRepository(persistentTokenRepository);
2.3.8 Thymeleaf中使用SpringSecurity
SpringSecurity可以在一些视图技术中进行控制显示效果。例如:JSP
或Thymeleaf
。在非前后端分离且使用SpringBoot的项目中多使用Thymeleaf作为视图展示技术。
1.引入依赖
<!--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>
在html页面中引入Thymeleaf命名空间和Security命名空间
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
编写demo
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.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>
</body>
</html>
在控制器里面添加页面跳转规则
@RequestMapping("/demo")
public String demo(){
return "demo";
}
启动程序,登录之后,在地址栏输入demo进入页面后页面显示
登录账号:admin
登录账号:admin
凭证:
权限和角色:[/main.html, ROLE_abc, admin, normal]
客户端地址:0:0:0:0:0:0:0:1
sessionId:D11A9509CBA602124F13D70B9B5AAFB4
2.3.9Thymeleaf中进行权限判断
- 应用场景,有增删改查4个按钮
- 判断用户是否具有某项权限
- 根据用户权限显示用户可用按钮
1.在自定义登录逻辑里面添加权限insert
,delete
// 赋予当前用户admin,normal权限
return new User(username,password,
AuthorityUtils
.commaSeparatedStringToAuthorityList("admin," +
"normal," +
"ROLE_abc,/main.html,/insert,/delete"));
在页面添加权限判断
<!DOCTYPE html>
<html lang="zh" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.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>
<br>
<hr>
通过权限判断:
<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>
<hr>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abc')">查找</button>
</body>
</html>
页面显示,符合预期,
2.3.10 退出登录
非常简单,只需要在任意页面添加退出登录超连接就可以了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录成功</title>
<style>
h1 {
color: black;
font-size: 45px;
text-align: center;
}
</style>
</head>
<body>
<h1>登录成功!</h1><br>
<a href="/logout">退出登录</a>
<a href="./main1.html">跳转</a>
</body>
</html>
退出登录之后,地址栏会携带logout参数,如果不想要这个参数,修改配置类
http.logout()
.logoutSuccessUrl("/login.html");
2.3.11 SpringSecurity中的CSRF
从刚开始学习SpringSecurity时,在配置类中一直存在这样一行代码:http.csrf().disable();
如果没有这行代码导致用户无法被认证。这行代码的含义是关闭csrf防护。
1.什么是CSRF
CSRF(Cross-Site Request Forgery)跨站请求伪造,也被称为"OneClick Attack"或者"Session Riding",通过伪造用户访问受信任站点的非法请求访问。
跨域:只要网络协议、ip地址、端口中任何一个不同就是跨站请求。
客户端与服务端进行交互的时候,由于http本身是无状态的协议,所以引入cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能会被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能会发生很多意想不到的事情。
2.SpringSecurity中的CSRF
从SpringSecurity4开始CSRF防护默认是开启的。默认会拦截请求。进行csrf处理。csrf为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
测试CSRF
注释http.csrf().disable();
// 关闭csrf防火墙
// http.csrf().disable();
在templates目录下准备一份login.html
去控制器编写页面跳转逻辑
@RequestMapping("showLogin")
public String showLogin(){
return "login";
}
修改自定义登录页面
// 自定义登录页面
// .loginPage("/login.html")
.loginPage("/showLogin")
记得授权认证url
.antMatchers("/showLogin").permitAll()
启动项目,输入用户名密码,我们进行正常操作
原因:访问时携带参数名为_csrf值为token,,否则无法访问。token是服务器生成的
<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="text" name="password"><br>
记住我:<input type="checkbox" name="remember-me" value="true"><br>
<button style="margin-left: 100px;margin-top: 20px" type="submit" value="登录">登录</button>
</form>
登录成功
抓包查看网路信息,请求头携带_csrf
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ehAZ65VF-1648992688054)(C:\Users\92518\AppData\Roaming\Typora\typora-user-images\image-20220403210955037.png)]
88052)]
2.3.10 退出登录
非常简单,只需要在任意页面添加退出登录超连接就可以了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录成功</title>
<style>
h1 {
color: black;
font-size: 45px;
text-align: center;
}
</style>
</head>
<body>
<h1>登录成功!</h1><br>
<a href="/logout">退出登录</a>
<a href="./main1.html">跳转</a>
</body>
</html>
退出登录之后,地址栏会携带logout参数,如果不想要这个参数,修改配置类
http.logout()
.logoutSuccessUrl("/login.html");
2.3.11 SpringSecurity中的CSRF
从刚开始学习SpringSecurity时,在配置类中一直存在这样一行代码:http.csrf().disable();
如果没有这行代码导致用户无法被认证。这行代码的含义是关闭csrf防护。
1.什么是CSRF
CSRF(Cross-Site Request Forgery)跨站请求伪造,也被称为"OneClick Attack"或者"Session Riding",通过伪造用户访问受信任站点的非法请求访问。
跨域:只要网络协议、ip地址、端口中任何一个不同就是跨站请求。
客户端与服务端进行交互的时候,由于http本身是无状态的协议,所以引入cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能会被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能会发生很多意想不到的事情。
2.SpringSecurity中的CSRF
从SpringSecurity4开始CSRF防护默认是开启的。默认会拦截请求。进行csrf处理。csrf为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
测试CSRF
注释http.csrf().disable();
// 关闭csrf防火墙
// http.csrf().disable();
在templates目录下准备一份login.html
[外链图片转存中…(img-wh4N66xB-1648992688053)]
去控制器编写页面跳转逻辑
@RequestMapping("showLogin")
public String showLogin(){
return "login";
}
修改自定义登录页面
// 自定义登录页面
// .loginPage("/login.html")
.loginPage("/showLogin")
记得授权认证url
.antMatchers("/showLogin").permitAll()
启动项目,输入用户名密码,我们进行正常操作
原因:访问时携带参数名为_csrf值为token,,否则无法访问。token是服务器生成的
<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="text" name="password"><br>
记住我:<input type="checkbox" name="remember-me" value="true"><br>
<button style="margin-left: 100px;margin-top: 20px" type="submit" value="登录">登录</button>
</form>
登录成功
抓包查看网路信息,请求头携带_csrf