前提:在看完这两篇文章的基础上再进行本篇文章的开发
(三)Spring Security (spring-boot-starter-security)应用详解
(四)spring-boot-starter-security 工作原理
前边的例子我们是将用户信息存储在内存中,实际项目中用户信息存储在数据库中,本节实现从数据库读取用户信息。根据前边对认证流程研究,只需要重新定义UserDetailService即可实现根据用户账号查询数据库。
创建user_db数据库
CREATE DATABASE `spring-security` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
创建t_user表
CREATE TABLE `t_user` (
`id` BIGINT(20) NOT NULL COMMENT '用户id',
`username` VARCHAR(64) NOT NULL COMMENT '用户登录账号',
`password` VARCHAR(64) NOT NULL COMMENT '用户登录密码',
`fullname` VARCHAR(255) NOT NULL COMMENT '用户姓名',
`mobile` VARCHAR(11) DEFAULT NULL COMMENT '手机号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT '用户表';
插入数据
INSERT INTO `t_user` (
`id`,
`username`,
`password`,
`fullname`,
`mobile`
)
VALUES
(
1,
'zhangsan',
'$2a$10$o/FkX0ss2mr6ligiO6AP9.z8ecTiBhJpqjew.iEGq4.s.V8r1UKRa',
'张三',
'16680804545'
),
(
2,
'lisi',
'$2a$10$InoDpP3VaYFNEPBbNrE3fOiEWFYgvIVLkuG/BzKF8jiPLjItGGFN6',
'李四',
'16680804545'
) ;
(一)添加依赖
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.0.5version>
dependency>
(二)在application.properties配置
spring.datasource.url=jdbc:mysql://192.168.88.128:3306/spring-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver‐class‐name=com.mysql.cj.jdbc.Driver
(三)entity 创建两个实体类
package com.cyj.security.springboot.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @program: Spring-Security-OAuth2
* @Description:
* @Author C_Y_J
* @create: 2021-01-29 11:05
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User {
private Long id;
private String username;
private String password;
private String fullname;
private String mobile;
}
package com.cyj.security.springboot.entity;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
/**
* @program: Spring-Security-OAuth2
* @Description:
* @Author C_Y_J
* @create: 2021-01-29 11:30
**/
@Getter
public class MyUserDetails extends User {
/**
* 这里我只用简单的 用户名 密码 权限集合
* 还有一个复杂的方法
*
* @param username
* @param password
* @param authorities
*/
public MyUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
}
(四) mapper
package com.cyj.security.springboot.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cyj.security.springboot.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
/**
* @program: Spring-Security-OAuth2
* @Description:
* @Author C_Y_J
* @create: 2021-01-29 11:18
**/
@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
}
(五) UserDetailService
package com.cyj.security.springboot.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cyj.security.springboot.entity.MyUserDetails;
import com.cyj.security.springboot.entity.User;
import com.cyj.security.springboot.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
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;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* @program: Spring-Security-OAuth2
* @Description:
* @Author C_Y_J
* @create: 2021-01-29 11:24
**/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (StringUtils.isEmpty(user)) {
throw new UsernameNotFoundException("用户不存在");
}
//我这里弄一个简单的权限集合,真实的开发中是要查询数据库的
List<GrantedAuthority> atuh = AuthorityUtils.commaSeparatedStringToAuthorityList("admin,p1");
//从数据库查出的用户信息赋值给MyUserDetails对象 ,MyUserDetails对象继承了security框架中user对象
return new MyUserDetails(
user.getUsername(),
user.getPassword(),
atuh
);
}
}
(六)BCryptPasswordEncoder
在安全配置类中定义BCryptPasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。
编写LoginController,修改loginSuccess方法,注意getUsername方法,Spring Security获取当前登录用户信息的方法为SecurityContextHolder.getContext().getAuthentication()
@RequestMapping(value = "/login-success", produces = {"text/plain;charset=utf-8"})
public String loginSuccess() {
String username = getUsername();
return username + " 登录成功";
}
/**
* 获取当前登录用户名
*
* @return
*/
private String getUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!authentication.isAuthenticated()) {
return null;
}
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
username =
((org.springframework.security.core.userdetails.UserDetails) principal).getUsername();
} else {
username = principal.toString();
}
return username;
}
我们可以通过以下选项准确控制会话何时创建以及Spring Security如何与之交互:
机制 | 描述 |
---|---|
always | 如果没有session存在就创建一个 |
ifRequired | 如果需要就创建一个Session(默认)登录时 |
never | SpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它。 |
stateless | SpringSecurity将绝对不会创建Session,也不使用Session |
/**
* 安全拦截机制(最重要)
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
//所有/r/**的请求必须认证通过
.antMatchers("/r/**").authenticated()
//除了/r/**,其它的请求可以访问
.anyRequest().permitAll();
http
//允许表单登录
.formLogin()
//自定义登录成功的页面地址
.successForwardUrl("/login-success");
http
//关闭CSRF控制
.csrf().disable();
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有效期为60s
spring boot 配置文件:
server.servlet.session.timeout=60s
session超时之后,可以通过Spring Security 设置跳转的路径。
http
.sessionManagement()
.expiredUrl("/login?error=EXPIRED_SESSION")
.invalidSessionUrl("/login?error=INVALID_SESSION");
expired指session过期,invalidSession指传入的sessionid无效。
我们可以使用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
Spring security默认实现了logout退出,访问
http://localhost:8080/security-spring-boot/logout
当退出操作出发时,将发生:
使HTTP Session 无效
清除 SecurityContextHolder
跳转到 /login
注意:如果让logout在GET请求下生效,必须关闭防止CSRF攻击csrf().disable()。如果开启了CSRF,必须使用post方式请求/logout
http
//关闭CSRF控制
.csrf().disable();
但是,类似于配置登录功能,咱们可以进一步自定义退出功能:
http
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.addLogoutHandler(logoutHandler)
.invalidateHttpSession(true);
(1)提供系统退出支持,使用 WebSecurityConfigurerAdapter 会自动被应用
(2)设置触发退出操作的URL (默认是 /logout ).
(3)退出之后跳转的URL。默认是 /login?logout 。
(4)定制的 LogoutSuccessHandler ,用于实现用户退出成功时的处理。如果指定了这个选项那么
logoutSuccessUrl() 的设置会被忽略。
(5)添加一个 LogoutHandler ,用于实现用户退出时的清理工作.默认SecurityContextLogoutHandler 会被添加为最后一个 LogoutHandler 。
(6)指定是否在退出时让 HttpSession 无效。 默认设置为 true。
一般来说, LogoutHandler 的实现类被用来执行必要的清理,因而他们不应该抛出异常。
下面是Spring Security提供的一些实现:
PersistentTokenBasedRememberMeServices基于持久化token的RememberMe功能的相关清理 TokenBasedRememberMeService基于token的RememberMe功能的相关清理 CookieClearingLogoutHandler 退出时Cookie的相关清理
CsrfLogoutHandler 负责在退出时移除csrfToken
SecurityContextLogoutHandler退出时SecurityContext的相关清理
链式API提供了调用相应的 LogoutHandler 实现的快捷方式,比如deleteCookies()。
原文链接