前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中。
本节实现从数据库读取用户信息。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。
创建user_db数据库:
CREATE DATABASE `user_db` CHARACTER SET 'utf8' COLLATE 'utf8_general_ci'
创建t_user表
CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL COMMENT '用户id', `username` varchar(64) NOT NULL, `password` varchar(64) NOT NULL, `fullname` varchar(255) NOT NULL COMMENT '用户姓名', `mobile` varchar(11) DEFAULT NULL COMMENT '手机号', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC
添加依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
在application.yaml配置
spring:
thymeleaf:
cache: false
datasource:
url: jdbc:mysql://localhost:3306/user_db?useSSL=false&useUnicode=true&characterEncoding=UTF-8
username: root
password: 18081736467xr
driver-class-name: com.mysql.jdbc.Driver
druid:
filters: stat,wall,slf4j
stat-view-servlet:
enabled: true
login-password: 18081736467xr
login-username: root
reset-enable: false
web-stat-filter:
enabled: true
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
url-pattern: /*
aop-patterns: com.atguigu.boot.*
filter:
stat:
slow-sql-millis: 1000
log-slow-sql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# mapper-locations: classpath:com.example.blog.dao.mapper/*.xml
# config-location: classpath:mybatisPlus/mybatis-config.xml
global-config: # 逻辑删除配置
db-config:
# 代表数据库表前缀为t_
table-prefix: t_
logic-delete-field: flag #全局逻辑删除字段值
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
数据库表对应的entity:
package com.example.springsecurity.entity;
import lombok.Data;
@Data
public class User
{
private String id;
private String username;
private String password;
private String fullname;
private String mobile;
}
UserDao:
package com.example.springsecurity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.springsecurity.entity.User;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserDao extends BaseMapper<User>
{
@Select("select * from t_user where username = #{userName} limit 1")
public User findUserByUserName(String userName);
}
UserDetailsServiceImpl:
package com.example.springsecurity.service;
import com.example.springsecurity.mapper.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
import com.example.springsecurity.entity.User;
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;
import java.util.List;
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException
{
User user = userDao.findUserByUserName(userName);
if(user == null)
return null;
return org.springframework.security.core.userdetails.User.withUsername(user.getUsername()).password(user.getPassword()).authorities("vip2").build();
// return User.withUsername().password("$2a$10$cqVXe8.rTerOSr7I3a5NZuIkFxTIvQcBCl3sSvs9qfyUcO9dd8/oK").authorities("vip2").build();
}
}
输入账号和密码请求认证,跟踪代码。
按照我们前面讲的PasswordEncoder的使用方法,使用BCryptPasswordEncoder需要完成如下工作:
1、在安全配置类中定义BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
2、loadUserByUsername方法:
是通过前端传递过来的password进行加密,再与数据库的password进行对比,所以数据库的password也需要加密才能比对成功,所以数据库中的密码应该存储BCrypt格式
会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。
spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
1.获取用户身份
编写LoginController,实现level的测试资源,并修改loginSuccess方法,注意getUsername方法,Spring Security获取当前登录用户信息的方法为SecurityContextHolder.getContext().getAuthentication()
@RequestMapping("login-success")
@ResponseBody
public String loginSuccess()
{
return getUserName() + "登陆成功";
}
//获取当前用户信息
private String getUserName()
{
String userName = "";
//获取当前认证通过的用户身份
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//用户身份
Object principal = authentication.getPrincipal();
if(principal == null)
{
userName = "匿名";
}
if(principal instanceof UserDetails)
{
UserDetails userDetails = (UserDetails) principal;
userName = userDetails.getUsername();
}else
{
assert principal != null;
userName = principal.toString();
}
return userName;
}
http.formLogin()
.usernameParameter("userName") //配置前端页面传递的用户名name
.passwordParameter("pwd") //配置前端页面传递的密码name
.loginPage("/toLogin") //定制登录页
.loginProcessingUrl("/login") //真正的登录请求,会跳转到spring Security登录验证界面
.successForwardUrl("/login-success")//登录成功跳转的url
2.会话控制
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
通过以下配置方式对该选项进行配置:
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
默认情况下,Spring Security会为每个登录成功的用户会新建一个Session,就是ifRequired 。
若选用never,则指示Spring Security对登录成功的用户不创建Session了,但若你的应用程序在某地方新建了 session,那么Spring Security会用它的。
若使用stateless,则说明Spring Security对登录成功的用户不会创建Session了,你的应用程序也不会允许新建 session。并且它会暗示不使用cookie,所以每个请求都需要重新进行身份验证。这种无状态架构适用于REST API 及其无状态认证机制。
会话超时
可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s; spring boot 配置文件
server.servlet.session.timeout=3600s
session超时之后,可以通过Spring Security 设置跳转的路径。
http.sessionManagement()
.expiredUrl("/login‐view?error=EXPIRED_SESSION")
.invalidSessionUrl("/login‐view?error=INVALID_SESSION");
expired指session过期,invalidSession指传入的sessionid无效。
安全会话cookie
我们可以使用httpOnly和secure标签来保护我们的会话cookie:
- httpOnly:如果为true,那么浏览器脚本将无法访问
- cookie secure:如果为true,则cookie将仅通过HTTPS连接发送
spring boot 配置文件:
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
退出
在WebSecurityConfig的protected void configure(HttpSecurity http)中配置:
@Override
protected void configure(HttpSecurity http) throws Exception
{
http
.authorizeRequests()
.and()
.logout() (1)
.logoutUrl("/logout") (2)
.logoutSuccessUrl("/login‐view?logout") (3)
.logoutSuccessHandler(logoutSuccessHandler) (4)
.addLogoutHandler(logoutHandler) (5)
.invalidateHttpSession(true); (6)
}
(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
(2)设置触发退出操作的URL (默认是 /logout ).
(3)退出之后跳转的URL。默认是 /login?logout 。
(4)定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么 logoutSuccessUrl() 的设置会被忽略。
(5)添加一个 LogoutHandler ,用于实现用户退出时的清理工作.默认 SecurityContextLogoutHandler 会被添加 为最后一个 LogoutHandler 。
(6)指定是否在退出时让 HttpSession 无效。 默认设置为 true。 注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF,必须使用 post方式请求/logout
logoutHandler:
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。
下面是Spring Security提供的一些实现:
- PersistentTokenBasedRememberMeServices 基于持久化token的RememberMe功能的相关清理
- TokenBasedRememberMeService 基于token的RememberMe功能的相关清理
- CookieClearingLogoutHandler 退出时Cookie的相关清理 CsrfLogoutHandler 负责在退出时移除
- csrfToken SecurityContextLogoutHandler 退出时SecurityContext的相关清理
链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如deleteCookies()。