Spring Security入门

1.Spring Security简介

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.

Spring Security是一个强大且可高度定制化的身份验证和访问控制框架。它事实上是一套保护基于Spring的应用程序的标准。

Spring Security是一个专注于为Java程序提供身份验证和授权的框架。与所有的Spring项目相同,Spring Security真正的强大之处在于可以轻松通过扩展来满足自定义需求。

​ 以上是Spring Security官方文档给出的简介。本人感觉使用Spring Security来进行身份认证和权限校验不会比不使用框架更加简单,因为Spring Security学习起来还是需要一定的成本的。Spring Security真正吸引我的还是它的扩展性,只要能够理清楚Spring Security中各个类的作用,在进行安全保护时就可以游刃有余。

​ 首先放一张图,这张图表示了Spring Security认证和授权的主干过程。

2.认证

首先放一下数据库的表结构,都是最简单的表结构,一共有五张表。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for resource
-- ----------------------------
DROP TABLE IF EXISTS `resource`;
CREATE TABLE `resource` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(32) NOT NULL COMMENT '资源名称',
  `url` varchar(255) DEFAULT NULL COMMENT '资源url',
  `pid` bigint(11) NOT NULL COMMENT '父资源id',
  `permission` varchar(255) DEFAULT NULL COMMENT '权限键值对',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='资源表';

-- ----------------------------
-- Records of resource
-- ----------------------------
BEGIN;
INSERT INTO `resource` VALUES (1, 'admin', '/test/admin', 0, NULL);
INSERT INTO `resource` VALUES (2, 'qa', '/test/qa', 1, NULL);
INSERT INTO `resource` VALUES (3, 'user', '/test/user', 1, NULL);
COMMIT;

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `code` varchar(32) NOT NULL COMMENT '角色编号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

-- ----------------------------
-- Records of role
-- ----------------------------
BEGIN;
INSERT INTO `role` VALUES (1, 'ROLE_ADMIN');
INSERT INTO `role` VALUES (2, 'ROLE_QA');
INSERT INTO `role` VALUES (3, 'ROLE_USER');
INSERT INTO `role` VALUES (4, 'ROLE_VISITOR');
COMMIT;

-- ----------------------------
-- Table structure for role_resource
-- ----------------------------
DROP TABLE IF EXISTS `role_resource`;
CREATE TABLE `role_resource` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `role_id` bigint(11) NOT NULL COMMENT '角色id',
  `resource_id` bigint(11) NOT NULL COMMENT '资源id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='角色资源关联表';

-- ----------------------------
-- Records of role_resource
-- ----------------------------
BEGIN;
INSERT INTO `role_resource` VALUES (1, 1, 1);
INSERT INTO `role_resource` VALUES (2, 1, 2);
INSERT INTO `role_resource` VALUES (3, 1, 3);
INSERT INTO `role_resource` VALUES (4, 2, 2);
INSERT INTO `role_resource` VALUES (5, 3, 3);
COMMIT;

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `username` varchar(32) NOT NULL COMMENT '用户名',
  `password` varchar(256) NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (1, 'a', '$2a$10$G1kdTbSEmsSzRo420UskqugXkvopV/rEmcwytfKGBbiAs99r1pREG');
INSERT INTO `users` VALUES (2, 'b', '$2a$10$8TLTtPLTBxYUh7ooH8Ep5.Tnp3/Okh0oOK8Q54/oJiv.Sa70YHkry');
INSERT INTO `users` VALUES (3, 'c', '$2a$10$u35irvlxXc67QmSOhoTmwOMh54aNVDw9vjxVWFdLC/.yDwHhMa51y');
COMMIT;

-- ----------------------------
-- Table structure for users_role
-- ----------------------------
DROP TABLE IF EXISTS `users_role`;
CREATE TABLE `users_role` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `users_id` bigint(11) NOT NULL COMMENT '用户id',
  `role_id` bigint(11) NOT NULL COMMENT '角色id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

-- ----------------------------
-- Records of users_role
-- ----------------------------
BEGIN;
INSERT INTO `users_role` VALUES (1, 1, 1);
INSERT INTO `users_role` VALUES (2, 2, 2);
INSERT INTO `users_role` VALUES (3, 3, 3);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

​ 用户表中的密码都是经过加密之后的,真实值与用户名相同。

​ 认证就是用户输入用户名和密码,然后校验的过程,按照上图所示,用户名和密码会被包装成Authentication类,然后用于验证。Authentication类中会包含用户的各种信息,如用户名、加密后的密码、用户所拥有的角色等等。

​ Authentication会在各个AuthenticationProvider中进行验证,由authenticate()方法进行验证,如下所示:

Authentication authenticate(Authentication authentication) throws AuthenticationException;

​ 只要有一个AuthenticationProvider验证通过,那么就说明用户认证成功。

​ AuthenticationProvider会由ProviderManager进行管理,ProviderManager中的authenticate()方法会循环调用AuthenticationProvider进行校验,authenticate()方法代码如下所示:

@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		int currentPosition = 0;
		int size = this.providers.size();
		for (AuthenticationProvider provider : getProviders()) {//循环调用所有的AuthenticationProvider
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				result = provider.authenticate(authentication);//调用AuthenticationProvider的authenticate()进行验证
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
		...
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
				parentResult = this.parent.authenticate(authentication);//如果有父ProviderManager,则调用父ProviderManager进行验证
				result = parentResult;
			}
		...
	}

​ 那么就有人会问,验证用户名和密码的正确性是在那里验证的呢,这个时候就要了解一个很重要的AuthenticationProvider,那就是DaoAuthenticationProvider。判断当前输入的用户名是否存在,需要我们实现UserDetailsService接口,重写loadUserByUsername()方法。

@Slf4j
@Service
public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements UsersService, UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<Users> queryUsers = Wrappers.lambdaQuery();
        queryUsers.eq(Users::getUsername, username);
        Users users = getOne(queryUsers);
        if (Objects.isNull(users)) {
            throw new UsernameNotFoundException(username + "not found");
        }
        return users;
    }
}

​ 此方法返回的UserDetails对象也需要我们自己定义,UserDetails是一个接口,我们需要实现该接口,并重写getAuthorities()方法,该接口的getAuthorities()方法返回的是当前用户所拥有的角色。

@Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UsersRoleService usersRoleService = BeanUtil.getBean(UsersRoleService.class);
        RoleService roleService = BeanUtil.getBean(RoleService.class);
        LambdaQueryWrapper<UsersRole> queryUsersRole = Wrappers.lambdaQuery();
        queryUsersRole.eq(UsersRole::getUsersId, id);
        List<Long> roleIds = usersRoleService.list(queryUsersRole).stream().map(UsersRole::getRoleId).collect(Collectors.toList());
        return roleService.listByIds(roleIds);
    }

​ 验证密码的正确性则是在DaoAuthenticationProvider中的additionalAuthenticationChecks()方法中进行的,不用我们手动实现。

@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		...
		String presentedPassword = authentication.getCredentials().toString();
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {//验证密码的正确性
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}

3.授权

​ 授权就是判断该用户有没有所访问url的链接。

​ 首先需要实现FilterInvocationSecurityMetadataSource接口,并重写getAttributes()方法,该方法会返回访问当前url所需的所有角色,只要当前登陆用户拥有这其中的任何一个角色都可以访问该url。

@Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        String requestUrl = ((FilterInvocation) object).getRequestUrl();

        List<Resource> resources = resourceService.list();
        List<String> allRole = new ArrayList<>();
        for (Resource resource : resources) {
            if (antPathMatcher.match(resource.getUrl(), requestUrl)) {
                LambdaQueryWrapper<RoleResource> queryRoleResource = Wrappers.lambdaQuery();
                queryRoleResource.eq(RoleResource::getResourceId, resource.getId());
                List<Long> roleIds = roleResourceService.list(queryRoleResource).stream().map(RoleResource::getRoleId).collect(Collectors.toList());
                List<String> roleCodes = roleService.listByIds(roleIds).stream().map(Role::getCode).collect(Collectors.toList());
                allRole.addAll(roleCodes);
            }
        }
        String[] roles = new String[allRole.size()];
        allRole.toArray(roles);
        return SecurityConfig.createList(roles);
    }

​ 最后由AccessDecisionManager来判断用户是否可以访问该url,需要我们实现AccessDecisionManager接口,并重写decide()方法。

@Override
    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
            throws AccessDeniedException, InsufficientAuthenticationException {
        if (authentication instanceof AnonymousAuthenticationToken) {
            throw new BadCredentialsException("用户未登录");
        }

        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        for (ConfigAttribute configAttribute : configAttributes) {
            String roleName = configAttribute.getAttribute();
            if ("ROLE_NONE".equals(roleName)) {
                return;
            }

            for (GrantedAuthority authority : authorities) {
                if ("ROLE_ADMIN".equals(authority.getAuthority())) {
                    return;
                }
                if (roleName.equals(authority.getAuthority())) {
                    return;
                }
            }
        }

        throw new AccessDeniedException("您无权访问:" + object);
    }

​ 如果用户拥有访问该url的角色,那么就放行,如果没有的话,会走到AccessDeniedHandler中,我们也可以自定义返回信息,只需要实现AccessDeniedHandler,并重写handle()方法。

@Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
            AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpStatus.FORBIDDEN.value());
        httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
        httpServletResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);

        PrintWriter writer = httpServletResponse.getWriter();
        writer.write("您无权限访问此URL,验证是否进入WebAccessDeniedHandler");
        writer.flush();
        writer.close();
    }

​ 最后,上述所有自定义的类,都需要配置到WebSecurityConfigurerAdapter中,我们可以继承WebSecurityConfigurerAdapter类,并重写里面的方法。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private BackdoorAuthenticationProvider backdoorAuthenticationProvider;
    @Resource
    private VisitorAuthenticationProvider visitorAuthenticationProvider;
    @Resource
    private UsersServiceImpl usersService;
    @Resource
    private WebSecurityMetadataSource webSecurityMetadataSource;
    @Resource
    private WebAccessDecisionManager webAccessDecisionManager;
    @Resource
    private WebAccessDeniedHandler webAccessDeniedHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(backdoorAuthenticationProvider)//自定义AuthenticationProvider
                .authenticationProvider(visitorAuthenticationProvider)//自定义AuthenticationProvider
                .userDetailsService(usersService)//相当于配置了一个DaoAuthenticationProvider
                .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/static/*");//表示哪些文件可以不认证
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setSecurityMetadataSource(webSecurityMetadataSource);
                        object.setAccessDecisionManager(webAccessDecisionManager);
                        return object;
                    }
                })
                .and().exceptionHandling().accessDeniedHandler(webAccessDeniedHandler)
                .and().formLogin().permitAll()
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/login").permitAll()
                .and().csrf().disable();
    }
}
posted @ 2021-10-18 21:18  饶一一  阅读(173)  评论(0编辑  收藏  举报