spring boot项目14:安全-基础使用-MySQL(1)

JAVA 8

Spring Boot 2.5.3

MySQL 5.7.21(单机)

---

授人以渔:

1、Spring Boot Reference Documentation

This document is also available as Multi-page HTML, Single page HTML and PDF.

有PDF版本哦,下载下来!

2、Spring Security Reference

有PDF版本哦(网页版末尾的 /html5/ 改为 /pdf/),下载下来!

目录

1、安全初体验

2、自定义表单登录页

3、多用户、角色、认证

使用InMemoryUserDetailsManager

使用JdbcUserDetailsManager

4、自定义数据库模型

用户过期试验

参考文档

本文使用项目:

mysql-hello

Web项目,底层使用MySQL存储数据,默认端口30000。

MySQL配置——后面会用到:

数据库配置
#
# MySQL on Ubuntu
spring.datasource.url=jdbc:mysql://mylinux:3306/db_example?serverTimezone=Asia/Shanghai
spring.datasource.username=springuser
spring.datasource.password=ThePassword
#spring.datasource.driver-class-name =com.mysql.jdbc.Driver # This is deprecated
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
# 打开使用过程中执行的SQL语句
spring.jpa.show-sql: true

1、安全初体验

添加依赖包 spring-boot-starter-security:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

包结构:

启动项目,此时,任何链接都不能访问。

启动日志:

Using generated security password 后面是 默认用户user的密码。

在浏览器中访问,弹出登录对话框:

登录页-源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Please sign in</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
</head>
<body>
<div>
<form method="post" action="/login">
<h2>Please sign in</h2>
<p>
<label for="username">Username</label>
<input type="text" name="username" placeholder="Username" required autofocus>
</p>
<p>
<label for="password">Password</label>
<input type="password" name="password" placeholder="Password" required>
</p>
<input name="_csrf" type="hidden" value="ed3f49ac-647f-4a59-b2e3-b24498725774" />
<button type="submit">Sign in</button>
</form>
</div>
</body></html>

源码里面有一个提交数据 /login 的表单——实现登录。

输入 user、日志中的密码,登录成功。

除了上面的 /login 实现登录,还有一个 /logout 端点实现 退出登录:

随机密码,而且存在日志里面,不好。配置下面的可以实现固定用户及密码:

# 安全
spring.security.user.name=lib
spring.security.user.password=123

再次启动,日志没有密码信息了。

浏览器登录,使用上面配置的 lib、123即可。

小结,

上面的项目很简单,但有一定实用性了

2、自定义表单登录页

登录页:login.html

static/login.html
<html>
<head>
<title>login:mysql-hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
body {
background: #ddd;
}
</style>
</head>
<body>
<div>请登录:</div>
<form action="login.html" method="post">
<div>用户名:<input type="text" name="username" placeholder="用户名" /></div>
<div>密码:<input type="password" name="password" placeholder="密码" /></div>
<div><a href="#">忘记密码?</a></div>
<div><input type="submit" value="登录" /> </div>
</form>
<br />
<br />
<div><a href="#">新用户注册</a></div>
</body>
</html>

注,包含username, password的<input>,注意<form>的action和method来自博客园

添加 AppWebSecurityConfig.java,继承 WebSecurityConfigurerAdapter 并重写 configure(HttpSecurity http)

@EnableWebSecurity
public class AppWebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义登录页:login.html
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// 自定义登录页
.loginPage("/login.html")
.permitAll()
.and()
.csrf().disable();
}
}

登录页面:

输入前面配置文件中的用户名、密码,登录成功(首页没有建,显示status=404),但可以测试其它链接的。

指定处理登录的URL-未通过

在formLogin()下,指定处理登录的URL:

.formLogin()
// 自定义登录页
.loginPage("/login.html")
// 处理登录请求的URL
.loginProcessingUrl("/login")

但是,测试失败,登录未成功。

错误信息
浏览器页面:
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Sat Sep 04 23:03:14 CST 2021
There was an unexpected error (type=Method Not Allowed, status=405).
---
应用日志:
Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method ‘POST‘ not supported]
Completed 405 METHOD_NOT_ALLOWED
"ERROR" dispatch for POST "/error", parameters={masked}

疑问

为什么呢?默认登录页的 action不就是 “/login” 吗?怎么这里配置了就不行呢?

像上面配置后,默认的/login 无效了?需要自己写?怎么写?格式呢?TODO

登录返回值

上面的试验中,登录成功后,跳转到首页。在真实的前后端分离系统中,登录后一般返回 成功与否的信息,比如,一段JSON数据,再由前端决定怎么处理——跳转到哪里。

在formLogin()下,配置 successHandler、failureHandler 分别实现登录成功、失败后的逻辑。来自博客园

.formLogin()
// 自定义登录页
.loginPage("/login.html")
// 处理登录请求的URL
// 指定后登录失败,注释掉,TODO
//				.loginProcessingUrl("/login")
// 登录成功的处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp,
Authentication auth) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(ResultVO.getSuccess("登录成功").toString());
}
})
// 登录失败的处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp,
AuthenticationException ex) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = resp.getWriter();
out.write(ResultVO.getFailed(HttpStatus.UNAUTHORIZED.value(), "登录失败", "请重新登录").toString());
}
})
.permitAll()
.and()

 注,ResultVO 是项目的一个 统一返回对象类,getSuccess、getFailed是其中的静态方法

测试结果:成功

3、多用户、角色、认证

前面的章节,只有一个用户。本章介绍多个用户的使用。

自定义一个 UserDetailsService Bean即可。

接口有很多实现类,其中:来自博客园

1)InMemoryUserDetailsManager 的用户数据 存储到 内存,重启后丢失

2)JdbcUserDetailsManager 的用户数据 存储到 数据库,比如,MySQL数据库

使用InMemoryUserDetailsManager 

准备3个接口:

/security/admin/hello 需要ADMIN角色的用户才可以访问

/security/user/hello 需要USER角色的用户才可以访问

/security/app/hello 任意登录用户都可以访问

SecurityAdminController.java
@RestController
@RequestMapping(value="/security/admin")
@Slf4j
public class SecurityAdminController {
@GetMapping(value="/hello")
public String hello() {
return "hello, Admin";
}
}

其它两个Controller类似。来自博客园

更改 AppWebSecurityConfig:

之前的configure函数做了改动;

增加了 UserDetailsService Bean的生成函数,并增加了2个用户对应不同的角色;

passwordEncoder函数 在 本文使用的 S.B.版本是必须的,否则发生异常,,但这个NoOpPasswordEncoder过期了,,原因及解决方案有待进一步研究,TODO

	/**
* 试验2:资源授权
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 使用角色
.antMatchers("/security/admin/**").hasRole("ADMIN")
.antMatchers("/security/user/**").hasRole("USER")
.antMatchers("/security/app/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().permitAll()
.and()
.csrf().disable();
}
/**
* 基于内存数据库的用户信息
*/
@Bean
public UserDetailsService userDetailsService() {
// 基于内存的用户信息:2个用户,不同角色
InMemoryUserDetailsManager man = new InMemoryUserDetailsManager();
man.createUser(User.withUsername("user").password("123").roles("USER").build());
man.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
return man;
}
/**
* 必须有,否则发生异常
* 是否可以使用其它 PasswordEncoder 的实现类呢?
* 据说是 5.X版本之后默认启用了 委派密码编码器 导致
* @author ben
* @date 2021-09-05 00:10:49 CST
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
// 过时了?怎么弄?TODO
// 因为不安全,只能用于测试、明文密码验证等,故废弃
return NoOpPasswordEncoder.getInstance();
}

注意,上面的配置后,配置文件中的 lib 用户就不能使用了

启动应用,测试:

user、admin分别访问前面的 3个接口。

用户/接口 user admin
/security/admin/hello type=Forbidden, status=403 hello, Admin
/security/user/hello hello, User type=Forbidden, status=403
/security/app/hello hello, APP hello, APP

符合预期。来自博客园

更进一步:

动态管理用户(增删改查),或可以使用 容器中的 userDetailsService Bean——即上面配置生成了。

转换为 InMemoryUserDetailsManager 后进行操作。

不过,应用重启后,这些用户数据丢失,意义不大,但从接口来看是可以做到的。

使用JdbcUserDetailsManager

引入:来自博客园

<!-- 使用JdbcUserDetailsManager时引入,没有JPA的吗? -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

注,本项目中,mysql-connector-java早已引入

在MySQL建立数据表:找到 JdbcUserDetailsManager 类 对应的jar包(spring-security-core),DDL文件位于 同一个jar包的 org.springframework.security.core.userdetails.jdbc.users.ddl

拷贝其中的语句,改其中的 varchar_ignorecase 为 varchar类型——MySQL支持。来自博客园

使用改造后的语句到MySQL终端去执行:下图展示执行成功,建立了两张表 users、authorities

改造 AppWebSecurityConfig 的userDetailsService函数:

	/**
* 使用JdbcUserDetailsManager
* 本应用的底层为 MySQL数据库——上面的dataSource
*/
@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager man = new JdbcUserDetailsManager();
System.out.println("dataSource=" + dataSource);
man.setDataSource(dataSource);
man.createUser(User.withUsername("user").password("123").roles("USER").build());
man.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
return man;
}

测试 两个用户对前面3个接口的权限:测试成功,符合预期

注,上面的 dataSource是 HikariPool-1:

JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default.
Therefore, database queries may be performed during view rendering. Explicitly configure
spring.jpa.open-in-view to disable this warning
dataSource=HikariDataSource (HikariPool-1)

启动后,新建数据表的数据:

注意,角色创建时是 user、admin,但在 数据库里面是 以“ROLE_”开头

再次启动应用,发生异常,启动失败,因为 user、admin在数据库中已经存在了。来自博客园

改造userDetailsService()函数:多了用户存在性判断

	@Bean
public UserDetailsService userDetailsService() {
JdbcUserDetailsManager man = new JdbcUserDetailsManager();
man.setDataSource(dataSource);
if (!man.userExists("user")) {
man.createUser(User.withUsername("user").password("123").roles("USER").build());
}
if (!man.userExists("admin")) {
man.createUser(User.withUsername("admin").password("123").roles("ADMIN").build());
}
return man;
}

默认的数据库模型肯定无法满足生产的需求,比如,里面的密码都没有加密。

Spring Security具有优良的扩展性,可以很好地实现自定义的数据库模型。

---210905 01:55---写到这儿了---

4、自定义数据库模型

在使用JdbcUserDetailsManager的默认数据库模型时,用户、权限是分成两张表的。来自博客园

本章介绍 基于自定义数据库模型的认证和授权。

两个步骤:1)实现UserDetails——用户详情;2)实现UserDetailsService——用户详情服务(类似于前面的2个Manager);

cofigure函数保持不变。

AppUser类,用户实体类,也实现了 UserDetails 接口。

AppUser.java
package org.lib.mysqlhello.security.self;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Transient;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义用户
* @author ben
* @date 2021-09-05 09:26:11 CST
*/
@Entity
@Data
@Slf4j
public class AppUser implements UserDetails {
private static final long serialVersionUID = 210905L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(columnDefinition = "VARCHAR(50) NOT NULL UNIQUE")
private String username;
@Column(columnDefinition = "VARCHAR(384) NOT NULL")
private String password;
/**
* 用户角色
* 多个角色使用英文都好(,)隔开
*/
@Column(columnDefinition = "VARCHAR(500) NOT NULL")
private String roles;
/**
* 用户是否启用:默认启用
*/
@Column(columnDefinition = "BIT(1) DEFAULT true")
private Boolean enabled;
/**
* 有效期时间戳
* 默认为0 永久有效
*/
@Column(columnDefinition = "BIGINT DEFAULT 0")
private Long expiration;
/**
* 创建时间
*/
@Column(insertable = false, columnDefinition = "DATETIME DEFAULT NOW()")
private Date createTime;
/**
* 更新时间
*/
@Column(insertable = false, updatable = false, columnDefinition = "DATETIME DEFAULT NOW() ON UPDATE NOW()")
private Date updateTime;
// ----实现UserDetails接口----
// set函数已使用 @Data 注解建立
@Transient
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
if (this.expiration <= 0) {
return true;
}
if (this.expiration >= System.currentTimeMillis()) {
return true;
}
log.warn("用户过期:id={}, expiration={}", this.id, this.expiration);
return false;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
// ----实现UserDetails接口----
}

启动应用,数据表建好了:

插入两条数据(用户):

-- 和之前不同,admin有两个角色哦
insert into app_user(username, password, roles) values("admin", "123", "ROLE_ADMIN,ROLE_USER");
insert into app_user(username, password, roles) values("user", "123", "ROLE_USER");

AppUserDetailsService类:实现了 UserDetailsService接口,并使用 @Service注解。来自博客园

@Service
public class AppUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}

上面的 AppUserDetailsService Bean 还无法使用:

前一章 的 userDetailsService() 函数也生成了 userDetailsService Bean,此时,虽然应用可以启动,但是,无法登录——因为有两个 userDetailsService Beans吧。

注释掉AppWebSecurityConfig类 的 userDetailsService() 函数。来自博客园

启动应用,登录:AppUserDetailsService 还没写完导致

继续改造 AppUserDetailsService...

改造后的 AppUserDetailsService:来自博客园

package org.lib.mysqlhello.security.self;
import java.util.Objects;
import java.util.function.Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* AppUserDetailsService
* @author ben
* @date 2021-09-05 10:34:23 CST
*/
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private AppUserDAO appUserDao;
private Consumer<Object> cs = System.out::println;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = appUserDao.findByUsername(username);
cs.accept("user 1=" + user);
if (Objects.isNull(user)) {
throw new UsernameNotFoundException("用户不存在");
}
// 权限集
// 使用Spring Security的AuthorityUtils:默认支持 英文逗号分开的权限集
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
cs.accept("user 1=" + user);
return user;
}
}

启动应用,测试已添加的用户admin、user访问各个接口:成功,符合预期来自博客园

UsernameNotFoundException说明

继承了 AuthenticationException——其下有若干的异常。

用户过期试验

在AppUserDetailsService#loadUserByUsername函数中抛出用户过期异常

失败了

看来不是这么用的。来自博客园

记得 AppUser 实现 UserDetails接口 时,有一个 isAccountNonExpired() 函数,或许,过期的判断已经实现了。

设置user过期时间——30秒有效期:

-- 当前时间+30秒过期
-- 注意使用 (unix_timestamp(now())+30)*1000!
-- 最开始只使用 now() 时验证失败/sad
mysql> update  app_user set expiration=(unix_timestamp(now())+30)*1000 where id = 2;

在执行上面的语句后,启动应用,使用 user登录:登录成功。

30秒后继续操作,可以继续操作,没有被阻止。TODO

30秒后,在另一个浏览器重新登录:登录失败,提示账号过期。

回到之前已登录的浏览器操作:可以继续,但会输出 isAccountNonExpired() 函数的 过期日志:来自博客园

可是,怎么阻止过期用户继续操作啊?!

》》》全文完《《《来自博客园

补充:

public interface UserDetails extends Serializable

其下的User类

public class User implements UserDetails, CredentialsContainer {

public interface UserDetailsService

后记:

密码没有加密啊?

阻止过期用户继续访问啊?来自博客园

记住用户?记住用户多长时间?

登录过程中都做了什么?过滤器、拦截器啥的?

自动登录呢?

基于token的登录呢?

……

看来,还要搞更多试验、更多学习才是啊!

后面再写一篇好了。来自博客园

参考文档

1、《Spring Security实战》

书,作者:陈木鑫,2019年8月第1版

非常感谢。

2、

spring boot项目14:安全-基础使用-MySQL(1)

原文:https://www.cnblogs.com/luo630/p/15204990.html

以上是spring boot项目14:安全-基础使用-MySQL(1)的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>