Spring Security使用数据库进行认证和授权
在前面的文章中,我们介绍了Spring Security基于内存的一些基本使用方法,但在真实的业务场景中,用户的账号、密码以及角色信息肯定都是存放在数据库中的,所以我们需要从数据库中来加载认证和授权的数据。
一、准备工作
如下案例是基于上一篇中的案例改造而来,所以建议先阅读前一篇文章的内容,将基本案例的代码准备好。
1.1 导入相关依赖
除了引入security的依赖外,我们还需要引入数据库驱动、连接池、MyBatis的依赖包:
1.2 配置信息
我们需要在配置文件中增加数据源的配置信息,指定mapper xml的位置,这里改成你自己的位置即可。
# 配置数据源信息 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://x.x.x.x:3306/zhangxun?characterEncoding=utf-8&&serverTimezone=Asia/Shanghai&&useSSL=false spring.datasource.username=root spring.datasource.password=root # 配置mapper xml的位置 mybatis.mapper-locations=classpath:mapper/*Mapper.xml # 是否显示执行sql logging.level.com.xun.mapper=debug
1.3 数据库准备
我们需要创建如下三张表,分别用来存储用户信息、角色信息、用户与角色的对应关系。
CREATE TABLE `auth_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(100) DEFAULT NULL, `password` varchar(100) DEFAULT NULL, `expired` tinyint(1) DEFAULT NULL, `locked` tinyint(1) DEFAULT NULL, PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 -- 密文密码的生成逻辑下面1.8节会讲到,可以替换为自己密码的密文进行插入 INSERT INTO zhangxun.auth_user (user_id, user_name, password, expired, locked) VALUES(1, 'root', '$2a$10$Hv037iGDdj82YjFORlYnyOdlra2EObV2XdddyW8A.r.Ph5ETOBNo2', 0, 0); INSERT INTO zhangxun.auth_user (user_id, user_name, password, expired, locked) VALUES(2, 'zhang', '$2a$10$cVgFLGx0Crz0Jf2TDBhJ.e6FS.BpH7YOoox2iSJrGW1DJ6OXiOt86', 0, 0); CREATE TABLE `auth_role` ( `role_id` int(11) NOT NULL AUTO_INCREMENT, `role_code` varchar(100) DEFAULT NULL, `role_name` varchar(100) DEFAULT NULL, PRIMARY KEY (`role_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; INSERT INTO zhangxun.auth_role (role_id, role_code, role_name) VALUES(1, 'admin', '管理员'); INSERT INTO zhangxun.auth_role (role_id, role_code, role_name) VALUES(2, 'manager', '经理'); CREATE TABLE `auth_user_role` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) DEFAULT NULL, `role_code` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4; -- root用户将拥有admin和manager两个角色,zhang用户仅拥有manager一个角色 INSERT INTO zhangxun.auth_user_role (id, user_id, role_code) VALUES(1, 1, 'admin'); INSERT INTO zhangxun.auth_user_role (id, user_id, role_code) VALUES(2, 1, 'manager'); INSERT INTO zhangxun.auth_user_role (id, user_id, role_code) VALUES(3, 2, 'manager');
1.4 实体类的创建
@Data public class Role { private Integer roleId; private String roleCode; private String roleName; } @Data public class User implements UserDetails { private Integer userId; private String userName; private String password; private Integer expired; private Integer locked; private Listroles; /** * 获取用户的所有角色信息 * @return */ @Override public Collection extends GrantedAuthority> getAuthorities() { List authorities = new ArrayList<>(); for(Role role : roles){ // 也可以在数据中添加角色时,就以 ROLE_ 开始,这样就不用二次添加了 authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleCode())); } return authorities; } /** * 指定哪一个是用户的密码字段 * @return */ @Override public String getPassword() { return password; } /** * 指定哪一个是用户的账户字段 * @return */ @Override public String getUsername() { return userName; } /** * 判断账户是否过期 * @return */ @Override public boolean isAccountNonExpired() { return (expired == 0); } /** * 判断账户是否锁定 * @return */ @Override public boolean isAccountNonLocked() { return (locked == 0); } /** * 判断密码是否过期 * 可以根据业务逻辑或者数据库字段来决定 * @return */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 判断账户是否可用 * 可以根据业务逻辑或者数据库字段来决定 * @return */ @Override public boolean isEnabled() { return true; } }
实现UserDetails接口的实体类会被认为是User实体,Spring Security会根据重写的方法来加载用户的必要信息:账户信息、密码信息、账户过期、账户锁定、密码过期、账户启用、账户拥有的角色信息。
1.5 Dao层的创建
@Mapper public interface UserMapper { User getUserByUserName(String userName); ListgetUserRolesByUserId(Integer userId); }
注意,如果UserMapper接口你是用了@Repository注解,那么就需要在启动类上加上Mapper所在包的位置。
@MapperScan("com.example.securitydemo.mapper")
1.6 Service层的编写
@Slf4j @Service public class UserService implements UserDetailsService { @Resource private UserMapper userMapper; /** * 根据用户名去数据库获取用户信息,SpringSecutity会自动进行密码的比对 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 用户名必须是唯一的,不允许重复 User user = userMapper.getUserByUserName(username); if(ObjectUtils.isEmpty(user)){ throw new UsernameNotFoundException("根据用户名找不到该用户的信息!"); } ListroleList = userMapper.getUserRolesByUserId(user.getUserId()); user.setRoles(roleList); return user; } }
1.7 Security配置
@EnableWebSecurity public class DBSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; /** * 对请求进行鉴权的配置 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 任何角色允许访问 .antMatchers("/", "/index").permitAll() // 仅admin角色可以访问 .antMatchers("/admin/**").hasRole("admin") // admin和manager两个角色可以访问 .antMatchers("/manager/**").hasAnyRole("admin", "manager") .and() // 没有权限进入内置的登录页面 .formLogin() .and() // 暂时关闭CSRF校验,允许get请求登出 .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } /** * 默认开启密码加密,前端传入的密码Security会在加密后和数据库中的密文进行比对,一致的话就登录成功 * 所以必须提供一个加密对象,供security加密前端明文密码使用 * @return */ @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
1.8 密码加密
@Slf4j public class PasswordEncode { private PasswordEncoder passwordEncoder; @Before public void before(){ this.passwordEncoder = new BCryptPasswordEncoder(); } @Test public void encodePassword(){ String rawPassword = "mm000"; String encodePassword = passwordEncoder.encode(rawPassword); log.info("password:{} encoded is: {}", rawPassword, encodePassword); } }
该类是一个测试类,为了将明文密码加密后得到密文的,密文存储到User表的password字段。Spring Security默认开启密码加密功能的,数据库加载出来的密码会被进行格式校验,如果不是合法的密文,登录逻辑就会失败。
1.9 测试结果
访问localhost:8080/index无需登录,直接就能返回结果。
访问localhost:8080/admin/getHello将跳转到登录页面,使用root账户(包含admin和manager两个角色),随便输入一个密码,登录失败;输入正确的密码,登录成功后返回正确的内容;此时再访问localhost:8080/manager/getHello也能获得正确的内容。
调用localhost:8080/logout后将退出登录,此时使用zhang账户(仅包含manager角色),输入正确的密码,登录成功后返回正确的内容;此时再访问localhost:8080/admin/getHello将无法获得内容。