SpringSecurity的基本上手学习

1. SpringSecurity的基本配置

  1. 导入SpringSecurity包

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  2. 只要导入了SpringSecurity,项目所有资源都会被保护起来了

    添加一个hello接口

    @GetMapping("/hello")
    public String hello() {
        return "Hello";
    }
    
  3. 启动项目后,访问/hello接口,会自动跳转到登录页面,这个登录页面是由SpringSecurity提供的。

    默认的用户名是user,默认的登录密码则在每次启动项目时随机生成,在终端打印。

    login-form.jpg

  4. 配置用户名和密码

    如果对于默认的用户名和密码不满意(肯定不满意,每次都是随机生成的密码),可以在配置文件中配置默认的用户名和密码,用户角色

    spring:
    	security:
        	user:
          		name: admin
          		password: 123
          		roles:
            		- admin
    

2. 基于内存的认证

  1. 通过自定义类继承WebSecurityConfigurerAdapter,实现configure(AuthenticationManagerBuilder auth)方法自定义,如下基于内存的认证:

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception{
        auth.inMemoryAuthentication()
            .passwordEncoder(new BCryptPasswordEncoder())
            .withUser("root")
            .password(new BCryptPasswordEncoder().encode("123"))
            .roles("ADMIN", "DBA")
            .and()
            .withUser("admin")
            .password(new BCryptPasswordEncoder().encode("123"))
            .roles("ADMIN", "USER")
            .and()
            .withUser("sang")
            .password(new BCryptPasswordEncoder().encode("123"))
            .roles("USER");
    }
    

    注意:在基于内存的用户配置中,配置角色时不需要添加"ROLE_"前缀!

    接下来的基于数据库的认证,角色需要带上ROLE_,比如ROLE_admin,ROLE_user

  2. WebSecurityConfigurerAdapter类有三个configure方法,方法参数不同,上面设置认证相关的是AuthenticationManagerBuilder,Http请求相关的需要重写参数为HttpSecurity的configure方法

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").access("hasAnyRole('ADMIN', 'USER')")
            .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")
            .anyRequest()
            .authenticated()
            .and()
            .formLogin()
            .loginPage("/login_page")  // 登录页面,默认为login_page
            .loginProcessingUrl("/login") // 登录请求接口,没有设置的话默认与loginPage一致
            .usernameParameter("name") // 请求参数用户名的参数名字
            .passwordParameter("passwd")	// 请求参数密码的参数名字
            .successHandler(new AuthenticationSuccessHandler() { // 登录成功的一些处理
                @Override
                public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    // 登录用户对象信息
                    Object principal = authentication.getPrincipal();
    
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    response.setStatus(200);
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 200);
                    map.put("msg", principal);
    
                    // json转换
                    ObjectMapper om = new ObjectMapper();
                    out.write(om.writeValueAsString(map));
                    out.flush();
                    out.close();
                }
            })
            .failureHandler(new AuthenticationFailureHandler() { // 登录失败的处理
                @Override
                public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter out = response.getWriter();
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 401);
                    if (exception instanceof LockedException) {
                        map.put("msg", "账户被锁定,登陆失败!");
                    } else if (exception instanceof BadCredentialsException) {
                        map.put("msg", "账户名或密码输入错误,登录失败!");
                    } else if (exception instanceof DisabledException) {
                        map.put("msg", "账户被禁用,登录失败!");
                    } else if (exception instanceof AccountExpiredException) {
                        map.put("msg", "账户已过期,登录失败!");
                    } else if (exception instanceof CredentialsExpiredException) {
                        map.put("msg", "密码已过期,登录失败!");
                    } else {
                        map.put("msg", "登录失败!");
                    }
    
                    ObjectMapper objectMapper = new ObjectMapper();
                    out.write(objectMapper.writeValueAsString(map));
                    out.flush();
                    out.close();
    
                }
            })
            .permitAll() // 表示和登录相关的接口不需要认证可直接访问
            // 注销登录
            .and()
            .logout()
            .logoutUrl("/logout")
            .clearAuthentication(true)  // 清除身份认证信息
            .invalidateHttpSession(true)    // 使session失效
            .addLogoutHandler(new LogoutHandler() {
                @Override
                public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
                    // 进行一些注销登录的处理
                }
            })
            .logoutSuccessHandler(new LogoutSuccessHandler() {
                @Override
                public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                    response.sendRedirect("/login_page");
                }
            })
            .and()
            .csrf()
            .disable();
    
    }
    

    代码解释:

    • 调用authorizeRequests()方法开启HttpSecurity的配置
    • antMatchers()hasRole()使用ANT匹配URL,表示访问“xxx"需要具备xxx角色

3. 基于数据库的认证

  1. 设计数据表,新建一个user,role以及user_role表

    db.jpg

    注意:角色名有一个默认的前缀:ROLE_

  2. 配置mybatis,数据库连接等

  3. 分别创建用户表和角色表对应的实体类

    @Data
    public class Role {
        private Integer id;
        private String name;
        private String nameZh;
    }
    
    @Data
    public class User implements UserDetails {
        private static final long serialVersionUID = 7740365774291023439L;
        private Integer id;
        private String username;
        private String password;
        private Boolean enabled;
        private Boolean locked;
        private List<Role> roles;
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            System.out.println("authorities = " + authorities);
            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;
        }
    }
    
    • 用户实体类需要实现UserDetails接口,并实现接口中的7个方法

      userDetails.jpg

    • 我们根据实际情况设置这7个方法的返回值,因为默认情况下不需要开发者自己进行密码角色等信息的比对,开发者只需要提供相关信息即可

    • getAuthorities()方法用来获取当前用户所具有的的角色信息,我们这里角色存储在roles属性中,所以这个方法里直接遍历roles属性,然后构造SimpleGrantedAuthority集合并返回

  4. 创建UserService,实现UserDetailsService接口

    @Service
    public class UserService implements UserDetailsService {
        @Autowired
        private UserMapper userMapper;
    
        @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()));
            System.out.println("user = " + user);
            return user;
        }
    }
    
    • 实现UserDetailsService接口,并实现了接口中的loadUserByUsername方法,该方法的参数就是用户登录时输入的用户名,通过用户名去查找数据库,如果查不到这个用户则抛出异常,如果查到了,则继续查找该用户具有的角色信息,并将获取到的user对象返回,由系统提供的DaoAuthenticationProvider类去比对密码是否正确

      也就是通过用户名查找用户,找得到就查下角色信息,将这个用户对象返回,系统会拿用户输入的密码与查询返回的用户对象密码去比对。

    • loadUserByUsername方法将在用户登录时自动调用

  5. 最后对SpringSecurity进行配置

    不再使用基于内存的配置方式了,将刚刚创建的UserService配置到AuthenticationManagerBuilder

    @Override
    protected  void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService);
    }
    

4. 角色继承

​ 在之前的例子中定义了三种角色,但是三种角色之间是没有任何关系的,一般来说角色权限是存在一定关系的,比如ROLE_admin一般是既具有admin权限,也具有user的权限的。

​ 如何配置这种角色继承关系?

在SpringSecurity中只需要提供一个RoleHierarchy即可

@Bean
RoleHierarchy roleHierarchy() {
    RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
    /**
     * 角色继承
     * @return
     */
    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        // springboot2.0.8(包含2.0.8)之前的格式
        // String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
        // springboot2.0.8之后版本的格式
        String hierarchy = "ROLE_dba > ROLE_admin \n ROLE_admin > ROLE_user";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }
}

注意:springboot2.0.8之后的版本中,写法是使用\n分隔符,之前的版本是使用空格

5. 动态配置权限

  1. 数据库设计

    新增一个menu表,存放资源路径以及一个资源角色表menu_role

    menu_db.jpg

  2. 自定义FilterInvocationSecurityMetadataSource

    Spring Security通过FilterInvocationSecurityMetadataSource接口中的getAttributes方法来确定一个请求需要哪些角色。

    FilterInvocationSecurityMetadataSource接口默认实现类是:DefaultFilterInvocationSecurityMetadataSource.参考该类的实现,可以定义自己的FilterInvocationSecurityMetadataSource.

    public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
        private AntPathMatcher antPathMatcher = new AntPathMatcher();
    
        @Autowired
        private MenuMapper menuMapper;
    
        @Override
        public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
            String requestUrl = ((FilterInvocation) object).getRequestUrl();
            List<Menu> allMenus = menuMapper.getAllMenus();
            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);
                }
            }
            return SecurityConfig.createList("ROLE_LOGIN");
        }
    
        @Override
        public Collection<ConfigAttribute> getAllConfigAttributes() {
            return null;
        }
    
        @Override
        public boolean supports(Class<?> clazz) {
            return FilterInvocation.class.isAssignableFrom(clazz);
        }
    }
    
  3. 自定义AccessDecisionManager

    当一个请求走完FilterInvocationSecurityMetadataSource的getAttributes方法后,接下来就会来到AccessDecisionManager类中进行角色信息的对比,可自定义AccessDecisionManager

    public class CustomAccessDecisionManager implements AccessDecisionManager {
        /**
         * 判断当前登录的用户是否具备当前请求url所需要的角色信息
         * @param authentication 当前登录用户的信息
         * @param object    是一个FilterInvocation对象,可以获取当前请求对象
         * @param configAttributes FIlterInvocationSecurityMetadataSource中的getAttributes方法的返回值,也就是当前请求url所需要的角色
         * @throws AccessDeniedException
         * @throws InsufficientAuthenticationException
         */
        @Override
        public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
            Collection<? extends GrantedAuthority> auths = authentication.getAuthorities();
            for (ConfigAttribute configAttribute : configAttributes) {
                if ("ROLE_LOGIN".equals(configAttribute.getAttribute()) && authentication 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;
        }
    }
    
  4. 配置

    完成两个自定义实现类后,需要在SpringSecurity中配置

    /**
     * 配置动态权限
     */
    @Bean
    public CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource() {
        return new CustomFilterInvocationSecurityMetadataSource();
    }
    
    @Bean
    public CustomAccessDecisionManager customAccessDecisionManager() {
        return new CustomAccessDecisionManager();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            //                .antMatchers("/admin/**").hasRole("admin")
            //                .antMatchers("/db/**").hasRole("dba")
            //                .antMatchers("/user/**").hasRole("user")
            //                .anyRequest().authenticated()
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
    
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                    object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource());
                    object.setAccessDecisionManager(customAccessDecisionManager());
                    return object;
                }
            })
            .and()
            .formLogin()
            .loginProcessingUrl("/login").permitAll()
            .and()
            .csrf().disable();
    }
    

注意:实现自定义动态权限,之前的角色继承就失效了,所需角色权限在数据库中配置即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值