【学习日志】SpringBoot整合SpringSecurity,SpringSecurity学习

这篇文章是在看了《Spring Boot+Vue全栈开发实战》后做的一些总结和笔记,要继续加油啊,奥利给!!!

1.创建一个SpringBootWeb项目,添加spring-boot-starter-security和Mybatis依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>

2.自定义类继承自WebSecurityConfigurerAdapter,实现对SpringSecurity的更多配置。

@Configuration
/**
 * @EnableGlobalMethodSecurity注解开启基于注解的安全配置,即面向方法的认证与授权,而非基于URL。
 * prePostEnabled=true会解锁@PreAuthorize和@PostAuthorize两个注解,分别在方法执行前和执行后进行验证。
 * securedEnabled=true会解锁@Secured注解。
 *
 * @Service
 * public class MethodService {
 *     @Secured("ROLE_ADMIN")
 *     访问该方法需要ADMIN角色,此方法需要在角色前加ROLE_。
 *     public String admin() {
 *         return "admin";
 *     }
 *     @PreAuthorize("hasRole('ADMIN') and hasRole('DBA')")
 *     public String dba() {
 *         return "dba";
 *     }
 *     @PreAuthorize("hasAnyRole('ADMIN','DBA','ADMIN')")
 *     public String user() {
 *         return "user";
 *     }
 * }
 */
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserService userService;
    @Autowired
    private CustomMetadataSource metadataSource;
    @Autowired
    private CustomAccessDecisionManager accessDecisionManager;

    /**
     * 不在AuthenticationManagerBuilder中配置passwordEncoder,可以直接使用@Bean配置。
     * 此处使用的加密方式:不加密。
     */
    /*@Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }*/

    /**
     * 角色继承
     * 若要ROLE_admin具有admin和user的权限,可用角色继承配置。
     */
    /*@Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        String hierarchy = "ROLE_dba > ROLE_admin ROLE_admin > ROLE_user";
        roleHierarchy.setHierarchy(hierarchy);
        return roleHierarchy;
    }*/

    /**
     * AuthenticationManagerBuilder用来配置全局的认证相关的信息,
     * 其实就是AuthenticationProvider和UserDetailsService,前者是认证服务提供者,后者是认证用户(及其权限)。
     * 
     * 这里配置了UserDetailsService(从数据库获取账户密码用于对比)和PasswordEncoder(密码加密方式)
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         * userService是一个实现了UserDetailsService接口的实体类。
         *
         * BCryptPasswordEncoder是官方推荐的,使用BCrypt强哈希函数。
         * 使用时可选择提供strength和SecureRandom实例。
         * strength越大,密钥迭代次数越多,迭代次数为^strength,strength取值在4~31之间默认为10。
         * 一般用户密码存在数据库中,所以要在注册时对密码进行加密处理:
         * BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
         * String encode = encoder.encode(password);
         */
        auth.userDetailsService(hrService).passwordEncoder(new BCryptPasswordEncoder());

        /*不使用UserDetailService,手动配置账户密码角色*/
        /*auth.inMemoryAuthentication()
                .withUser("admin").password("123").roles("ADMIN","USER")
                .and()
                .withUser("123").password("123").roles("USER");*/
    }


    /**
     * HttpSecurity 具体的权限控制规则配置,例如什么URL需要什么角色。
     * 可在此配置受保护资源,以及根据实际情况进行角色管理。
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /* 把权限控制规则放在数据库中,数据库中的角色需要ROLE_前缀!!! */
        http.authorizeRequests()
                /*在定义FilterSecurityInterceptor时,将动态权限配置的2个实例设置进去*/
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(metadataSource);
                        object.setAccessDecisionManager(accessDecisionManager);
                        return object;
                    }
                })
                .and()
                .formLogin().loginPage("/login_p").loginProcessingUrl("/login")
                /*定义登录成功的处理逻辑*/
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest
                            , HttpServletResponse httpServletResponse
                            , Authentication authentication) throws IOException, ServletException {
				  httpServletResponse.setContentType("application/json;charset=utf-8");
                  /*通过Authentication可以获取当前登录用户的信息*/
                  Object principal = authentication.getPrincipal();
                  /*Authentication也可以通过SecurityContextHolder.getContext().getAuthentication()获得*/
                  Object principal1 = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
                  ObjectMapper objectMapper = new ObjectMapper();
                  String value = objectMapper.writeValueAsString(principal);
                  PrintWriter writer = httpServletResponse.getWriter();
                  writer.write(value);
                  writer.flush();
                  writer.close();
                    }
                })
                /*定义登录失败的逻辑*/
        	 	/*可通过AuthenticationException获取登录失败的原因,进而给用户明确提示。*/
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest
                            , HttpServletResponse httpServletResponse
                            , AuthenticationException e) throws IOException, ServletException {
                  httpServletResponse.setContentType("application/json;charset=utf-8");
                  String respBean = "";
                  if (e instanceof BadCredentialsException || e instanceof UsernameNotFoundException) {
                     respBean = "用户名或密码错误!";
                  } else if (e instanceof LockedException) {
                     respBean = "账户被锁定,请联系管理员!";
                  } else if (e instanceof CredentialsExpiredException) {
                     respBean = "密码过期,请联系管理员!";
                  } else if (e instanceof AccountExpiredException) {
                     respBean = "账户过期,请联系管理员!";
                  } else if (e instanceof DisabledException) {
                     respBean = "账户被禁用,请联系管理员!";
                  } else {
                     respBean = "登录失败!";
                  }
        
                  /*401:用户没有访问权限*/
                  httpServletResponse.setStatus(401);
                  ObjectMapper objectMapper = new ObjectMapper();
                  String value = objectMapper.writeValueAsString(respBean);
                  PrintWriter writer = httpServletResponse.getWriter();
                  writer.write(value);
                  writer.flush();
                  writer.close();
                    }
                })
                .permitAll()
                .and()
                .logout().permitAll()
                .and()
                /*关闭csrf*/
                .csrf().disable()
                });

        /*直接在方法中手动确定权限规则控制*/
        /*authorizeRequests()开启HttpSecurity的配置*/
        //        http.authorizeRequests()
        //                .antMatchers("/admin/**")
        //                .hasRole("ADMIN")
        //                .antMatchers("/user/**")
        //                .access("hasAnyRole('ADMIN','USER')")
        //                .antMatchers("/db/**")
        //                .access("hasRole('ADMIN') and hasRole('DBA')")
        //                //除了前面配置过的URL路径外,访问其他URL必须登录后访问
        //                .anyRequest().authenticated()
        //                .and()
        //                //formLogin()开启表单登录。
        //                .formLogin()
        //                //loginPage("/login_page"):若用户访问一个需授权才能访问的接口,会跳转至login_page这个自定义页面进行登录,而非默认登录页面。
        //                .loginPage("/login_page")
        //                //loginProcessingUrl("/login")配置了登录接口为"/login"(即调用/login接口,发起POST请求进行登录)。
        //                .loginProcessingUrl("/login")
        //                //接口的登录参数中用户名默认是username,密码默认是password,可通过usernameParameter和passwordParameter手动配置。
        //                .usernameParameter("username").passwordParameter("password")
        //                //permitAll()表示和登录相关的接口不需要认证
        //                .permitAll()
        //                .and()
        //                //开启注销登录的配置
        //                .logout()
        //                //配置注销登录请求URL,默认是"/logout"
        //                .logoutUrl("/logout")
        //                //配置注销是否清除身份认证信息,默认为ture,表示清除
        //                .clearAuthentication(true)
        //                //配置注销是否使Session失效,默认为ture
        //                .invalidateHttpSession(true)
        //                //配置一个LogoutHandler,可以在其中完成一些数据清除工作,例如Cookie的清除
        //                //SpringSecurity提供了一些常见的LogoutHandler的实现类
        //                .addLogoutHandler(new LogoutHandler() {
        //                    @Override
        //                    public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
        //                        //数据清除
        //                    }
        //                })
        //                //定义注销成功后的逻辑
        //                .logoutSuccessHandler(new LogoutSuccessHandler() {
        //                    @Override
        //                    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        //                        //注销后的业务逻辑
        //                    }
        //                });
    }
}

3.基于数据库的认证——定义Role类实现UserDetails接口的User类

在这里插入图片描述

public class Role{
    private Integer id;
    private String name;
    private String nameZh;

    //getter and setter
}
public class User implements UserDetails {
    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<GrantedAuthority> list = new ArrayList<>();
        for (Role role : roles) {
            list.add(new SimpleGrantedAuthority(role.getName()));
        }
        return list;
    }

	@JsonIgnore
    @Override
    /*获取当前对象密码*/
    /*前端用户在登录成功后,需要获取当前用户的信息,对于一些敏感信息不必返回,使用@JsonIgnore注解即可*/
    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;
    }

    //getter and setter
}

4. 基于数据库的认证——定义实现UserDetailsService接口的Service层

@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;

    /**
     * 本类实现UserDetailsService接口,实现了loadUserByUsername方法,参数是用户登录输入的用户名。
     * 通过用户名去数据库查找用户,若没找到用户,抛出用户不存在异常;
     * 若找到用户,继续查找用户所具有的角色信息,将获取到的用户对象返回,再由系统提供的DaoAuthenticationProvider类去对比密码是否正确。
     * loadUserByUsername方法在用户登录时自动调用。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.loadUserWithRolesByUsername(username);
        System.out.println("loadUserByUsername: " + hr);
        if (user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }
        return user;
    }
}

5.基于数据库的认证——UserMapper,RoleMapper

(1)实现UserDetailsService接口的Service层所用到的UserMapper,包含根据用户名查找用户(含角色信息)的方法。
public interface UserMapper {
    /**
     * 根据username查询User实体
     */
    @Select("select * from user where username=#{username}")
    @Results({
            @Result(property = "id", column = "id", id = true),
            @Result(property = "roles", column = "id", many = @Many(select = "com.zio.dao.RoleMapper.findRolesByUserId"))
    })
    User loadUserWithRolesByUsername(@Param("username") String username);
}
(2)UserMapper中loadUserWithRolesByUsername方法用到的RoleMapper
public interface RoleMapper {
    @Select("select r.* from user_role ur,role r where ur.rid=r.id and ur.userid=#{userId}")
    List<Role> findRolesByUserId(@Param("userId") Integer userId);
    
    /*此方法在后面被MenuMapper调用*/
    @Select("select r.* from menu_role mr,role r where mr.rid=r.id and mr.mid=#{menuId}")
    List<Role> findRolesByMenuId(@Param("menuId") Integer menuId);
}

6.动态权限配置——定义Menu类

在这里插入图片描述

public class Menu{
    private Integer id;
    private String pattern;

    private List<Role> roles;

	//gettter and setter
}

7.动态权限配置——自定义FilterInvocationSecurityMetadataSource

/**
 * FilterInvocationSecurityMetadataSource的默认实现类是DefaultFilterInvocationSecurityMetadataSource。
 * 一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes方法后,会来到AccessDecisionManager类中进行角色信息的对比。
 */
@Component
public class CustomMetadataSource implements FilterInvocationSecurityMetadataSource {
    @Autowired
    private MenuMapper menuMapper;

    /**
     * getAttributes方法确定一个请求需要哪些角色。
     *
     * 用户访问时,通过此方法获取所访问的URL所需的角色。
     * 此方法若返回null,代表这个请求不需要任何角色就能访问。
     * 注意:登录处理过程的URL不走此方法。
     */
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String url = ((FilterInvocation) object).getRequestUrl();
        if ("/login_p".equals(url)) {
            return null;
        }
        /*创建一个AntPathMatcher,用于实现ant风格的URL匹配*/
        AntPathMatcher matcher = new AntPathMatcher();
        List<Menu> allMenu = menuMapper.findAllMenus();
        for (Menu menu : allMenu) {
            if (matcher.match(menu.getUrl(), url) && !CollectionUtils.isEmpty(menu.getRoles())) {  //URL匹配成功且需要角色
                System.out.println("匹配成功: menuUrl:" + menu.getUrl() + " url:" + url);
                List<Role> roles = menu.getRoles();
                int size = roles.size();
                String[] array = new String[size];
                for (int i = 0; i < size; i++) {
                    array[i] = roles.get(i).getName();
                }
                return SecurityConfig.createList(array);
            }
        }
        //若未匹配成功的URL,均为登录后访问
        return SecurityConfig.createList("ROLE_LOGIN");
    }

    /**
     * 返回定义好的权限资源,SpringSecurity在启动时校验相关配置是否正确,若不需校验,返回null即可。
     */
    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    /**
     * 判断类对象是否支持校验。
     */
    @Override
    public boolean supports(Class<?> clazz) {
        //源类.isAssignableFrom(目标类)   用于判断目标类是否是源类的子类
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

8.动态权限配置——自定义AccessDecisionManager

/**
 * 一个请求走完FilterInvocationSecurityMetadataSource中的getAttributes方法后,会来到AccessDecisionManager类中进行角色信息的对比。
 */
@Component
public class CustomAccessDecisionManager implements AccessDecisionManager {
    /**
     * 判断当前登录的用户是否具备当前请求URL所需要的角色信息
     * 不具备,抛出AccessDeniedException异常
     * 具备,不做任何事
     *
     * @param authentication   包含当前登录用户的信息
     * @param object           是一个FilterInvocation对象,可以获取当前请求URL等
     * @param configAttributes FilterInvocationSecurityMetadataSource中getAttributes方法的返回值,即当前请求URL所需要的角色
     * @throws AccessDeniedException
     * @throws InsufficientAuthenticationException
     */
    @Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        //循环判断访问该URL所需的每个角色,用户拥有其中一个即可访问
        for (ConfigAttribute configAttribute : configAttributes) {
            String needRole = configAttribute.getAttribute();
            if ("ROLE_LOGIN".equals(needRole)) {  //只需登录就能访问
                if (authentication instanceof AnonymousAuthenticationToken) {  //判断authentication是否为匿名用户实例
                    throw new AccessDeniedException("未登录");
                } else {  //已登录即放行
                    return;
                }
            }

            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                if (authority.getAuthority().equals(needRole)) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("权限不足");
    }

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

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

9.动态权限配置——MenuMapper

public interface MenuMapper {
    @Select("select * from menu")
    @Results({
            @Result(property = "id", column = "id", id = true),
            @Result(property = "roles", column = "id", many = @Many(select = "com.zio.dao.RoleMapper.findRolesByMenuId"))
    })
    List<Menu> findAllMenus();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值