Java教程

SpringBoot Spring Security基于数据库的认证

本文主要是介绍SpringBoot Spring Security基于数据库的认证,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

添加如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.10</version>
</dependency>

 

数据库连接配置:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://localhost:3306/boot?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

mybatis:
  mapper-locations:
    - classpath:mapper/**/*.xml

 

创建实体类:

public class User implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Boolean enabled;
    private Boolean locked;
    private List<Role> roles;

    /*
     * 用户实体类需要实现UserDetails接口,并实现该接口中的7个方法
     * 用户根据实际情况设置这7个方法的返回值。因为默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可,
     * 例如getPassword()方法返回的密码和用户输入的登录密码不匹配,会自动抛出BadCredentialsException异常,
     * isAccountNonExpired()方法返回了false,会自动抛出AccountExpiredException异常,
     * 因此对开发者而言,只需要按照数据库中的数据在这里返回相应的配置即可。本案例因为数据库中只有enabled和locked字段,故账户未过期和密码未过期两个方法都返回true。
     */

    // 获取当前用户对象所具有的角色信息

    /**
     * getAuthorities()方法用来获取当前用户所具有的角色信息,
     * 本案例中,用户所具有的角色存储在roles属性中,因此该方法直接遍历roles属性,然后构造SimpleGrantedAuthority集合并返回。
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    // 获取当前用户对象的密码
    @Override
    public String getPassword() {
        return password;
    }

    // 获取当前用户对象的用户名
    @Override
    public String getUsername() {
        return username;
    }

    // 当前账户是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 当前账户是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    // 当前账户密码是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 当前账户是否可用
    @Override
    public boolean isEnabled() {
        return enabled;
    }

    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 Boolean getEnabled() {
//        return enabled;
//    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }

    public Boolean getLocked() {
        return locked;
    }

    public void setLocked(Boolean locked) {
        this.locked = locked;
    }

    public List<Role> getRoles() {
        return roles;
    }

    public void setRoles(List<Role> roles) {
        this.roles = roles;
    }
}

@Data
public class Role {
    private Integer id;
    private String name;
    private String nameZh;
}

@Data
public class Menu {

    private Integer id;
    private String pattern;
    private List<Role> roles;

}

  用户实体类需要实现UserDetails接口,并实现该接口中的7个方法

 

 

创建UserService:

@Service
public class UserService implements UserDetailsService {

    @Autowired
    UserMapper userMapper;

    /**
     * 定义UserService实现UserDetailsService接口,
     * 并实现该接口中的loadUserByUsername方法,该方法的参数就是用户登录时输入的用户名,通过用户名去数据库中查找用户,
     * 如果没有查找到用户,就抛出一个账户不存在的异常,
     * 如果查找到了用户,就继续查找该用户所具有的角色信息,
     * 并将获取到的user对象返回,
     * 再由系统提供的DaoAuthenticationProvider类去比对密码是否正确。
     * <p>
     * loadUserByUsername方法将在用户登录时自动调用。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("账户不存在!");
        }
        user.setRoles(userMapper.getUserRolesByUid(user.getId()));
        return user;
    }
}

  

配置Spring Security:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserService userService;

    // 角色继承
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 没有配置内存用户,而是将刚刚创建好的UserService配置到AuthenticationManagerBuilder中。
        auth.userDetailsService(userService);
    }

    @Order
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()

                /*.antMatchers("/admin/**").hasRole("ADMIN")//表示用户访问“/admin/**”模式的URL必须具备ADMIN的角色
                .antMatchers("/user/**").access("hasAnyRole('ADMIN','USER')")
                .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
                .anyRequest().authenticated()//表示除了前面定义的URL模式之外,用户访问其他的URL都必须认证后访问(登录后访问)*/

                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(cfisms());//动态配置权限
                        object.setAccessDecisionManager(cadm());//角色信息的比对
                        return object;
                    }
                })

                .and()
                .formLogin()
                .loginProcessingUrl("/login").permitAll()
                .and()
                .csrf().disable();
    }

    @Bean
    CustomFilterInvocationSecurityMetadataSource cfisms() {
        return new CustomFilterInvocationSecurityMetadataSource();
    }

    @Bean
    CustomAccessDecisionManager cadm() {
        return new CustomAccessDecisionManager();
    }
}

  

角色继承:

案例中定义了三种角色,但是这三种角色之间不具备任何关系,一般来说角色之间是有关系的,例如ROLE_admin一般既具有admin的权限,又具有user的权限。那么如何配置这种角色继承关系呢?在Spring Security中只需要开发者提供一个RoleHierarchy即可

 

动态配置权限:

使用HttpSecurity配置的认证授权规则还是不够灵活,无法实现资源和角色之间的动态调整,要实现动态配置URL权限,就需要开发者自定义权限配置

自定义FilterInvocationSecurityMetadataSource:

@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    AntPathMatcher antPathMatcher = new AntPathMatcher(); // AntPathMatcher,主要用来实现ant风格的URL匹配。

    @Autowired
    MenuMapper menuMapper;

    // Spring Security中通过FilterInvocationSecurityMetadataSource接口中的getAttributes方法来确定一个请求需要哪些角色,
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        /*
         * 该方法的参数是一个FilterInvocation,开发者可以从FilterInvocation中提取出当前请求的URL,
         * 返回值是Collection<ConfigAttribute>,表示当前请求URL所需的角色。
         */
        String requestUrl = ((FilterInvocation) object).getRequestUrl();//从参数中提取出当前请求的URL。
        /*
         * 从数据库中获取所有的资源信息,即本案例中的menu表以及menu所对应的role,
         * 在真实项目环境中,开发者可以将资源信息缓存在Redis或者其他缓存数据库中。
         */
        List<Menu> allMenus = menuMapper.getAllMenus();
        // 遍历资源信息,遍历过程中获取当前请求的URL所需要的角色信息并返回。
        for (Menu menu : allMenus) {
            if (antPathMatcher.match(menu.getPattern(), requestUrl)) {
                List<Role> roles = menu.getRoles();
                String[] roleArr = new String[roles.size()];
                for (int i = 0; i < roleArr.length; i++) {
                    roleArr[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(roleArr);
            }
        }
        // 如果当前请求的URL在资源表中不存在相应的模式,就假设该请求登录后即可访问,即直接返回ROLE_LOGIN。
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     * getAllConfigAttributes方法用来返回所有定义好的权限资源,SpringSecurity在启动时会校验相关配置是否正确,
     * 如果不需要校验,那么该方法直接返回null即可。
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     *  supports方法返回类对象是否支持校验。
     */
    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

  

自定义AccessDecisionManager:

/**
 * AccessDecisionManager类中进行角色信息的比对
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {

    /**
     * 重写decide方法,在该方法中判断当前登录的用户是否具备当前请求URL所需要的角色信息,如果不具备,就抛出AccessDeniedException异常,否则不做任何事即可。
     *
     * @param auth   当前登录用户的信息
     * @param object FilterInvocation对象,可以获取当前请求对象
     * @param ca     FilterInvocationSecurityMetadataSource中的getAttributes方法的返回值,即当前请求URL所需要的角色
     */
    @Override
    public void decide(Authentication auth, Object object, Collection<ConfigAttribute> ca) {
        Collection<? extends GrantedAuthority> auths = auth.getAuthorities();
        for (ConfigAttribute configAttribute : ca) {
            /*
             * 如果需要的角色是ROLE_LOGIN,说明当前请求的URL用户登录后即可访问
             * 如果auth是UsernamePasswordAuthenticationToken的实例,那么说明当前用户已登录,该方法到此结束,否则进入正常的判断流程
             */
            if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && auth instanceof UsernamePasswordAuthenticationToken) {
                return;
            }
            for (GrantedAuthority authority : auths) {
                // 如果当前用户具备当前请求需要的角色,那么方法结束
                if (configAttribute.getAttribute().equals(authority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足");
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
}

  

附:

-- ----------------------------
-- Table structure for menu
-- ----------------------------
CREATE TABLE `menu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pattern` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('1', '/db/**');
INSERT INTO `menu` VALUES ('2', '/admin/**');
INSERT INTO `menu` VALUES ('3', '/user/**');

-- ----------------------------
-- Table structure for menu_role
-- ----------------------------
CREATE TABLE `menu_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `mid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

-- ----------------------------
-- Records of menu_role
-- ----------------------------
INSERT INTO `menu_role` VALUES ('1', '1', '1');
INSERT INTO `menu_role` VALUES ('2', '2', '2');
INSERT INTO `menu_role` VALUES ('3', '3', '3');

-- ----------------------------
-- Table structure for role
-- ----------------------------
CREATE TABLE `role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) DEFAULT NULL,
  `nameZh` varchar(32) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'dba', '数据库管理员');
INSERT INTO `role` VALUES ('2', 'admin', '系统管理员');
INSERT INTO `role` VALUES ('3', 'user', '用户');

-- ----------------------------
-- Table structure for user
-- ----------------------------
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(32) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `enabled` tinyint(1) DEFAULT NULL,
  `locked` tinyint(1) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('2', 'admin', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');
INSERT INTO `user` VALUES ('3', 'sang', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq', '1', '0');

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
CREATE TABLE `user_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

-- ----------------------------
-- 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');

  

参考: Spring Boot+Vue全栈开发实战 - 10.2 基于数据库的认证

 

这篇关于SpringBoot Spring Security基于数据库的认证的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!