重点标识
通过Jdbc整合Spring Security
通过MyBatis整合Spring Security
Jdbc
创建一个Spring boot工程,如下:
UserDetailsService 提供用户,主要就是这个:
这个案例,我准备用Jdbc搞一下,我们看一下Jdbc里面的
我们可以看到,这里面已经定义好了sql,甚至表名也已经给我们定义好了,我们只需要按照他的要求建表就行了,实际上这几张表都非常简单,完全可以分析出来他都有哪些字段,以及字段类型,但是,如果不想分析,Security也提供了相应的ddl文件,在core里面,路径如下:
如果你使用的是和我一样的版本,可以直接用如下sql:
create table users(username varchar(50) not null primary key,password varchar(500) not null,enabled boolean not null);
create table authorities (username varchar(50) not null,authority varchar(50) not null,constraint fk_authorities_users foreign key(username) references users(username));
create unique index ix_auth_username on authorities (username,authority);
好了,我们现在来看JdbcUserDetailsManager,基于Jdbc来做,首先,配置注入一个JdbcUserDetailsManager,注意,这里JdbcTemplate 可以自动注入,因为这次我们使用的是Spring Boot项目,在properties中配置好数据库的连接地址,用户名,密码后,Spring Boot会自动帮我们注入到容器中。
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql:///security_jdbc
这里配置两个用户,作为初始化用户。
@Bean
JdbcUserDetailsManager jdbcUserDetailsManager(JdbcTemplate jdbcTemplate){
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager();
jdbcUserDetailsManager.setJdbcTemplate(jdbcTemplate);
jdbcUserDetailsManager.createUser(User.withUsername("admin").password("{noop}123").roles("admin").build());
jdbcUserDetailsManager.createUser(User.withUsername("user").password("{noop}123").roles("user").build());
return jdbcUserDetailsManager;
}
在之前Spring分析中,有说过自动化配置,实际上,看这里, new JdbcTemplate(dataSource); Spring Boot已经帮我们自动注入到Spring容器中了,按照它的规范,就不需要我们再自己配置了。
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnMissingBean({JdbcOperations.class})
class JdbcTemplateConfiguration {
JdbcTemplateConfiguration() {
}
@Bean
@Primary
JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
JdbcProperties.Template template = properties.getTemplate();
jdbcTemplate.setFetchSize(template.getFetchSize());
jdbcTemplate.setMaxRows(template.getMaxRows());
if (template.getQueryTimeout() != null) {
jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
}
return jdbcTemplate;
}
}
但是这样写,还有一个问题,首次启动是没有问题,但是如果再次启动,就会报错,因为每次启动项目,相当于都是往数据库插入一条同样的数据,这样肯定是不行的。
稍微修改一下,这样就ok了。
@Bean
JdbcUserDetailsManager jdbcUserDetailsManager(JdbcTemplate jdbcTemplate){
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager();
jdbcUserDetailsManager.setJdbcTemplate(jdbcTemplate);
if(!jdbcUserDetailsManager.userExists("admin")){
jdbcUserDetailsManager.createUser(User.withUsername("admin").password("{noop}123").roles("admin").build());
}
if(!jdbcUserDetailsManager.userExists("user")){
jdbcUserDetailsManager.createUser(User.withUsername("user").password("{noop}123").roles("user").build());
}
return jdbcUserDetailsManager;
}
MyBatis
UserDetails 制定了规范,由我们来实现。
创建一个Spring Boot项目,引入依赖如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
新建User类,以及相关service,Dao,如下:
User类
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean accountNonExpired;
private Boolean accountNonLocked;
private Boolean credentialsNonExpired;
//get与is是重复的,反射会报错,所以删掉
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public void setAccountNonExpired(Boolean accountNonExpired) {
this.accountNonExpired = accountNonExpired;
}
public void setAccountNonLocked(Boolean accountNonLocked) {
this.accountNonLocked = accountNonLocked;
}
public void setCredentialsNonExpired(Boolean credentialsNonExpired) {
this.credentialsNonExpired = credentialsNonExpired;
}
/**
* 返回用户的权限/角色
* 角色也可以从这里获取
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 获取用户的密码
* @return
*/
@Override
public String getPassword() {
return password;
}
/**
* 获取用户名
* @return
*/
@Override
public String getUsername() {
return username;
}
/**
* 账户是否没有过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
/**
* 账户是否没有被锁定
* @return
*/
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
/**
* 密码是否没有过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
/**
* 账户是否被锁定
* @return
*/
@Override
public boolean isEnabled() {
return enabled;
}
}
UserService类
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
/**
* 当用户登录的时候,会自动触发到这里,去数据库查询
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User u = userMapper.loadUserByUsername(username);
if(u == null){
throw new UsernameNotFoundException("用户名不存在!");
}
return u;
}
}
UserDao类
@Mapper
public interface UserMapper {
User loadUserByUsername(String username);
}
xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.tongzhou.srcurity_mybatis.mapper.UserMapper">
<select id="loadUserByUsername" resultType="org.tongzhou.srcurity_mybatis.model.User">
SELECT * FROM USER WHERE USERNAME=#{username}
</select>
</mapper>
我这里是放到了resources下面,所以需要在properties配置一下,顺带着把数据库也配置一下。
mybatis.mapper-locations=classpath*:/mapper/*.xml
spring.datasource.url=jdbc:mysql:///security_mybatis
spring.datasource.username=root
spring.datasource.password=123456
数据库这里我建立了三张表,user role,user_role,
role表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(0) NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`nameZh` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_dba', '数据库管理员');
INSERT INTO `role` VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `role` VALUES (3, 'ROLE_user', '用户');
SET FOREIGN_KEY_CHECKS = 1;
user表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(0) NULL DEFAULT NULL,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`enabled` tinyint(1) NULL DEFAULT NULL,
`accountNonExpired` tinyint(1) NULL DEFAULT NULL,
`accountNonLocked` tinyint(1) NULL DEFAULT NULL,
`credentialsNonExpired` tinyint(1) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (2, 'zhangsan', '{noop}123', 1, 1, 1, 1);
INSERT INTO `user` VALUES (3, 'lisi', '{noop}123', 1, 1, 1, 1);
SET FOREIGN_KEY_CHECKS = 1;
user_role关联表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(0) NOT NULL,
`uid` int(0) NULL DEFAULT NULL,
`rid` int(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);
SET FOREIGN_KEY_CHECKS = 1;
如此,便可大功告成,登录了。
这里还有一点要记录下,我们知道,在service里面,抛出了一个用户找不到异常,实际上,为了避免外界的攻击,一般不会给到很明确的回复,比如说,直接给一个登陆失败或者,账户或密码错误。
这一点,Spring Security也替我们考虑到了。
这是怎么回事呢,我们可以打个断点,进入到源码里面瞅一瞅:
接着往下走,就会走到下面这部分,有一个hideUserNotFoundExceptions,也就是说,会将“用户找不到”这个异常直接隐藏掉,这样就完美帮助我们解决掉了可能因为提示而导致的漏洞问题了。
至于说,错误提示中文这部分,就是国际化了,有兴趣的可以点进去看看:
结语
努力,终点就在前方!