熵增学院-Jacob-死灰复燃的Security

一直以来,Spring系列给人的感觉都是快速,简洁,好理解,易操作.但Security是一个特例,这个框架相比而言,首先就是复杂,其次是灵活性也不够.好在于是Spring出的,因此与Spring配合比较好.并且在Spring的大力推广和支持下,它仍然屹立在这里.当然它也有自己的优点,比如他与LDAP还有Oauth这些结构的集成,处理的也不错.我们今天主要从以下几个方面来分享关于Security的知识:

基础使用

与OAuth2.x的集成

1. Security的基础使用

在web应用的设计中,权限是一个绕不开的话题.而在web权限设计中,RBAC是最流行的设计思路了.(除了RABC,还有像Linux中的ACL权限设计).在RBAC这种设计思路的引导下,我们可以有很多种实现方式,从最简单的一个过滤器开始,到Security或Shiro,甚至和其他的第三方进行集成,都是没有问题的.今天我们就先来看看Spring Security怎么使用.

1.1 SpringBoot中Security的默认配置

我们创建一个SpringBoot项目,然后引入spring-security,pom中的依赖如下所示:

<dependencies>

    <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>

</dependencies>

然后我们添加一个测试的接口,如下所示:

@RestController

@RequestMapping("users/")

public class UserInfoController {

    @GetMapping("hello")

    public String hello(){

        return "HelloWorld";

    }

}

最后是我们的启动类,其实启动类并没有任何改变:

@SpringBootApplication

public class Application {

    public static void main(String[] args) {

        SpringApplication.run(Application.class,args);

    }

}

我们启动,就会发现在日志里,他给我们生成了这样的一段内容:

2018-11-13 13:42:15.307  INFO 13084 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'

2018-11-13 13:42:15.620  INFO 13084 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: b74fd02a-0ad2-40ec-b6cd-3f2edfa015c1

2018-11-13 13:42:15.756  INFO 13084 --- [           main] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f13811b, org.springframework.security.web.context.SecurityContextPersistenceFilter@22d7fd41, org.springframework.security.web.header.HeaderWriterFilter@4fc165f6, org.springframework.security.web.csrf.CsrfFilter@65514add, org.springframework.security.web.authentication.logout.LogoutFilter@3bc69ce9, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@1ca610a0, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@79980d8d, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@59fc6d05, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@1775c4e7, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@19fd43da, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@2785db06, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@78307a56, org.springframework.security.web.session.SessionManagementFilter@5a7df831, org.springframework.security.web.access.ExceptionTranslationFilter@750f64fe, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@1f9d4b0e]

2018-11-13 13:42:15.878  INFO 13084 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''

2018-11-13 13:42:15.885  INFO 13084 --- [           main] top.lianmengtu.security.Application      : Started Application in 4.07 seconds (JVM running for 4.73)

这里给我们生成了一个密码,它是做什么的呢?我们现在来访问我们的测试接口,然后就会有一个登录窗口让你登录.这是怎么回事儿呢?

http://localhost:8080/users/hello

这是因为当我们添加了security模块后,SpringBoot默认为我们启用了security的拦截,并且如果我们没有配置默认的用户名密码的话,他就给我们生成了一个默认的用户名user,而密码则就是我在上面的日志中.当我们完成登录后,我们就可以正常使用我们的接口了.

1.2 SpringBoot中SpringSecurity的简易配置

现在我们来对SpringSecurity进行自定义用户名密码配置,我们创建一个application.yml,然后设置如下:

spring:

  security:

    user:

      name: zhangsan

      password: zhangsan123

然后重启我们的应用,我们会发现,SpringBoot不在给我们提供默认密码了,而当我们访问我们的接口的时候,我们可以使用新配置的zhangsan和zhangsan123进行登录.这样的配置主要是由SpringSecurity中的WebSecurityConfig来实现的,因此我们也可以将用户名和密码写到那里面,这里就不给大家演示了.

1.3 Security的一些自定义实现

但这种方式仍然不够灵活,通常我们都会考虑由我们自己来定义用户信息以及权限信息,其实用户信息与权限信息也是权限框架关注的两个主要点,这两点也被称为认证及授权.所谓认证,通俗点来讲,就是登录校验,确定访问用户的凭据是否正确.所谓授权就是该合法用户是否拥有对应的权限.

这是我们就需要问一个问题,Spring Security如何处理url与权限的匹配,也就是说Spring Security他如何知道哪些url是可以被公开访问,哪些url登录后可以访问,哪些还需要某些固定的权限才可以访问?

1.3.1 用户认证

这些问题的答案就在WebSecurityConfigurerAdapter里,在这个Adapter里有两个configure函数,一个是configure(HttpSecurity http),主要作用是配置哪些url可以直接放过,哪些是需要登录才能访问的,另外一个是configure(AuthenticationManagerBuilder auth),这个函数主要是用来做用户认证的,我们现在先来写url的映射与拦截.如下所示:

package top.lianmengtu.security.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests().anyRequest().authenticated();

    }


}

此时如果我们访问我们的接口,就会出现403的场景,如下所示,有没有很熟悉的感觉:

Whitelabel Error Page

This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Nov 14 17:58:11 CST 2018

There was an unexpected error (type=Forbidden, status=403).

Access Denied

只是这样还不行,因为我们希望能够放过一些接口,比如登录,然后其他的希望让用户登录之后能够进行访问.我们先来改造一下我们的url处理接口configure(HttpSecurity http).

@Override

protected void configure(HttpSecurity http) throws Exception {

    http.authorizeRequests().antMatchers("/auth/login").permitAll()//放过登录接口

            .and().formLogin().loginProcessingUrl("/auth/login")//指定登录处理接口

            .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登录成功与登录失败的处理器

            .and().authorizeRequests().anyRequest().authenticated()//对其他接口的权限限制为登录后才能访问

            .and().csrf().disable();//禁用csrf拦截,如果使用restclient和postman测试,建议禁掉,要不然会出错

}

此时就需要用到configure(AuthenticationManagerBuilder auth)这个函数了.我们可以透过这个函数注入一个UsersDetailsService,然后在UserDetailsService里来进行准确的处理.此时完成的SecurityConfig如下所示:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import top.lianmengtu.security.handler.LoginFailHandler;

import top.lianmengtu.security.handler.LoginSuccessHandler;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-13 16:01

 **/

@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired

    private MyUserDetailServiceImpl userDetailService;

    @Autowired

    private LoginSuccessHandler loginSuccessHandler;

    @Autowired

    private LoginFailHandler loginFailHandler;

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests().antMatchers("/auth/login").permitAll()//放过登录接口

                .and().formLogin().loginProcessingUrl("/auth/login")//指定登录处理接口

                .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登录成功与登录失败的处理器

                .and().authorizeRequests().anyRequest().authenticated()//对其他接口的权限限制为登录后才能访问

                .and().csrf().disable();//禁用csrf拦截,如果使用restclient和postman测试,建议禁掉,要不然会出错

    }

    @Override

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());

    }

    @Bean

    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();

    }

}

UserDetailsService的实现如下:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.stereotype.Component;

import top.lianmengtu.security.users.model.UserInfo;

import top.lianmengtu.security.users.service.IUserInfoService;

@Component

public class MyUserDetailServiceImpl implements UserDetailsService {

    @Autowired

    private IUserInfoService userInfoService;

    @Override

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        System.out.println("----Hello");

        System.out.println("name:"+username);

        if(!username.equals("zhangsan")){

            throw new UsernameNotFoundException("用户名不对");

        }

        UserInfo userInfo=userInfoService.loadByNickName(username);

        return User.withUsername(username).password(userInfo.getPassword()).roles("ADMIN").build();

    }

}

UserDetailsService里有一个userInfoService,这个是我们临时自定义的一个接口,我们可以在这个接口里接入数据库,这里只是一个简单的实现,如下所示:

package top.lianmengtu.security.users.service;

import top.lianmengtu.security.users.model.UserInfo;

public interface IUserInfoService {

    public UserInfo loadByNickName(String nickName);

}

package top.lianmengtu.security.users.service.impl;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.stereotype.Service;

import top.lianmengtu.security.users.model.UserInfo;

import top.lianmengtu.security.users.service.IUserInfoService;

@Service

public class UserInfoService implements IUserInfoService {

    @Autowired

    PasswordEncoder passwordEncoder;

    @Override

    public UserInfo loadByNickName(String nickName) {

        UserInfo userInfo=new UserInfo();

        userInfo.setNickName("zhangsan");

        userInfo.setPassword(passwordEncoder.encode("123456"));

        return userInfo;

    }

}

那登录成功或失败的处理逻辑呢?我们在SecurityConfig里添加了LoginSuccessHandler和LoginFailHandler,也只是一个简单的实现,这里仅供参考:

package top.lianmengtu.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.core.AuthenticationException;

import org.springframework.security.web.authentication.AuthenticationFailureHandler;

import org.springframework.stereotype.Component;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.util.HashMap;

import java.util.Map;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-14 16:41

 **/

@Component

public class LoginFailHandler implements AuthenticationFailureHandler {

    @Override

    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {

        ObjectMapper objectMapper=new ObjectMapper();

        httpServletResponse.setContentType("application/json;charset=UTF-8");

        Map<String,String> result=new HashMap<>();

        result.put("code","-1");

        result.put("msg","用户名/密码错误,请重新登录");

        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(result));

    }

}

package top.lianmengtu.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.core.Authentication;

import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import org.springframework.stereotype.Component;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-14 14:01

 **/

@Component

public class LoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override

    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {

        ObjectMapper objectMapper=new ObjectMapper();

        httpServletResponse.setContentType("application/json;charset=UTF-8"); // 响应类型

        httpServletResponse.getWriter().write(objectMapper.writeValueAsString("登录验证成功"));

        System.out.println("-----login successful:"+objectMapper.writeValueAsString(authentication.getDetails()));

    }

}

然后我们启用restclient进行测试,就可以得到我们预期的结果了.

1.3.2 用户授权

现在我们完成了用户的认证,那授权如何处理呢?其实在我们刚刚所展示出来的UserDetailsService里,有一个roles,这里描述的是用户的角色,我们现在只需要做两件事就可以了.第一件就是获取当前请求的url及其需要的角色,第二件就是与当前用户的角色进行比较并作出放行或者拦阻的操作.

在SpringSecurity中,进行url拦截的是FilterInvocationSecurityMetadataSource,这里我们自定义一个MyFilterInvocationSecurityMetadataSource,主要用于拿到当前url所需要的角色信息,并将这个url对应的角色信息传入到下一个组件AccessDecisionManager中然后与用户所拥有的角色进行比较,如果url没有找到或者url没有角色信息,这里添加了一个默认的登录认证,也可以直接放行.代码如下所示:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.access.ConfigAttribute;

import org.springframework.security.access.SecurityConfig;

import org.springframework.security.web.FilterInvocation;

import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;

import org.springframework.stereotype.Component;

import top.lianmengtu.security.users.service.IAuthorityService;

import java.util.Arrays;

import java.util.Collection;

import java.util.List;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-15 09:37

 **/

@Component

public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    @Autowired

    private IAuthorityService authorityService; //接入自定义的Service

    @Override

    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {

        String requestUrl=((FilterInvocation)object).getRequestUrl();

        System.out.println("----->MyFilterInvocationSecurityMetadataSource:拿到了url:"+requestUrl);

        if(requestUrl.equals("/auth/login")){ //登录接口,直接放过

            return null;

        }

        List<String> roleList=authorityService.findRolesByUrl(requestUrl);

        if(roleList.size()>0){

            String[] roleArray=new String[roleList.size()];

            for (int i = 0; i < roleList.size(); i++) {

                roleArray[i]=roleList.get(i);

            }

            return SecurityConfig.createList(roleArray);

        }

        return SecurityConfig.createList("ROLE_LOGIN"); //其他接口设置为登录放行,也可以像登录接口一样直接放过

    }

    @Override

    public Collection<ConfigAttribute> getAllConfigAttributes() {

        return null;

    }

    @Override

    public boolean supports(Class<?> clazz) {

        return FilterInvocation.class.isAssignableFrom(clazz);

    }

}

在这个实现中我们注入了我们自己的AuthorityService,这里实现的比较简单,大家可以根据需要从自己的数据库里对数据进行查找,Service示例如下所示:

package top.lianmengtu.security.users.service;

import java.util.List;

public interface IAuthorityService {

    public List<String> findRolesByUrl(String url);

}

-------------------------------------------------------

package top.lianmengtu.security.users.service.impl;

import org.springframework.stereotype.Service;

import top.lianmengtu.security.users.service.IAuthorityService;

import java.util.ArrayList;

import java.util.List;

@Service

public class AuthorityServiceImpl implements IAuthorityService {

    @Override

    public List<String> findRolesByUrl(String url) {

        System.out.println("-----url:"+url);

        List<String> rolesList=new ArrayList<>();

        rolesList.add("ADMIN");

        rolesList.add("MANAGER");

        return rolesList;

    }

}

当FilterInvocationSecurityMetadataSource的操作完成之后,他会将这个角色列表传入到AccessDecisionManager中,在其中与登录用户的角色进行比较,然后决定放过还是拦截,自定义AccessDecisionManager代码如下所示:

package top.lianmengtu.security.config;

import org.springframework.security.access.AccessDecisionManager;

import org.springframework.security.access.AccessDeniedException;

import org.springframework.security.access.ConfigAttribute;

import org.springframework.security.authentication.AnonymousAuthenticationToken;

import org.springframework.security.authentication.BadCredentialsException;

import org.springframework.security.authentication.InsufficientAuthenticationException;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.GrantedAuthority;

import org.springframework.stereotype.Component;

import java.util.Collection;

import java.util.Iterator;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-15 09:55

 **/

@Component

public class MyAccessDecisionManager implements AccessDecisionManager {

    @Override

    public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {

        Iterator<ConfigAttribute> configAttributeIterator = configAttributes.iterator(); //获取上个组件中传过来的角色集合

        while (configAttributeIterator.hasNext()){

            ConfigAttribute configAttribute=configAttributeIterator.next();

            String role=configAttribute.getAttribute();

            if("ROLE_LOGIN".equals(role)){ //判断是否需要具备登录权限

                if(authentication instanceof AnonymousAuthenticationToken){

                    throw new BadCredentialsException("未登陆");

                }

                return;

            }

            Collection<? extends GrantedAuthority> currentUserAuthorities=authentication.getAuthorities();//获取用户的角色信息

            for(GrantedAuthority grantedAuthority: currentUserAuthorities){

                if(grantedAuthority.getAuthority().equals(role)){//如果用户的角色信息包含了当前链接所需要的角色,则放行

                    return;

                }

            }

        }

        throw new AccessDeniedException("权限不足");

    }

    @Override

    public boolean supports(ConfigAttribute attribute) {

        return true;

    }

    @Override

    public boolean supports(Class<?> clazz) {

        return true;

    }

}

现在我们已经做好了用户授权的操作,现在是以异常的形式来展现结果,我们可以对结果进行处理,将结果转换为我们期望的json形式然后返回给前台,这是由AccessDeniedHandler来处理的,自定义AccessDeniedHandler的代码如下所示:

package top.lianmengtu.security.handler;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.security.access.AccessDeniedException;

import org.springframework.security.web.access.AccessDeniedHandler;

import org.springframework.stereotype.Component;

import javax.servlet.ServletException;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import java.io.IOException;

import java.io.PrintWriter;

import java.util.HashMap;

import java.util.Map;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-15 10:06

 **/

@Component

public class MyAccessDeniedHandler implements AccessDeniedHandler {

    @Override

    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {

        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);

        httpServletResponse.setCharacterEncoding("UTF-8");

        PrintWriter out = httpServletResponse.getWriter();

        Map<String,String> result=new HashMap<>();

        result.put("code","-1");

        result.put("msg","权限不足");

        ObjectMapper objectMapper=new ObjectMapper();

        String resultString=objectMapper.writeValueAsString(result);

        out.write(resultString);

        out.flush();

        out.close();

    }

}

截止到现在,我们的准备工作已经做完了,现在我们来进行最后一步整合的操作,我们修改一下我们自定义的那个WebSecurityConfigurerAdapter组件中的configure(HttpSecurity http)函数,来使我们的处理真正生效,代码如下所示:

package top.lianmengtu.security.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.context.annotation.Import;

import org.springframework.security.config.annotation.ObjectPostProcessor;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;

import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;

import top.lianmengtu.security.handler.LoginFailHandler;

import top.lianmengtu.security.handler.LoginSuccessHandler;

import top.lianmengtu.security.handler.MyAccessDeniedHandler;

/**

 * @program test_security

 * @description

 * @author: Jacob.Li

 * @create: 2018-11-13 16:01

 **/

@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired

    private MyUserDetailServiceImpl userDetailService;

    @Autowired

    private LoginSuccessHandler loginSuccessHandler;

    @Autowired

    private LoginFailHandler loginFailHandler;

    @Autowired

    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired

    private MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource;

    @Autowired

    private MyAccessDecisionManager myAccessDecisionManager;

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http.authorizeRequests()

                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {//这里的ObjectPostProcessor可以完全挪出去,像其他的handler一样

                    @Override

                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {

                        object.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource);//配置我们刚刚自定义好的FilterInvocationSecurityMetadataSource,来加载url所需要的角色

                        object.setAccessDecisionManager(myAccessDecisionManager);//配置我们刚刚自定义好的AccessDecisionManager,来进行用户角色和url所需角色的对比

                        return object;

                    }

                })

                .antMatchers("/auth/login").permitAll()//放过登录接口

                .and().formLogin().loginProcessingUrl("/auth/login")//指定登录处理接口

                .successHandler(loginSuccessHandler).failureHandler(loginFailHandler)//指定登录成功与登录失败的处理器

                .and().authorizeRequests().anyRequest().authenticated()//对其他接口的权限限制为登录后才能访问

                .and().csrf().disable()

                .exceptionHandling().accessDeniedHandler(myAccessDeniedHandler);//授权失败的处理

    }

    @Override

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());

    }

    @Bean

    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();

    }

}

好了,我们的授权工作已经完成了,现在我们可以重新启动我们的项目,然后可以用restClient访问我们的接口进行测试了.

2. OAuth2.0

我们可以看到,基本授权使用Spring Security是比较复杂的,我们完全可以只用一个Filter加几个自定义注解完成这项工作,这也是很多人在开发一个单体项目时所做的一件事儿.这里之所以提到单体项目是因为在多个项目之间,如果我们想要进行权限限制,就不能在这么做了,尤其是我们将我们的部分资源公开提供给其他人使用的时候,这样的一刀切权限可能就不太适合.

当多个应用之间共享权限的时候,我们往往会关注两个问题,第一是权限的安全性,第二,是权限的粒度.什么意思呢?比如说我们提供了一个资源管理服务器,里面有照片、视频、文档,然后大家可以往我们的资源服务器上传自己的资源.后来用户又在另外一个网站上需要上传自己的头像,但他不想用默认的头像,想用自己在资源服务器上的照片,那么此时,如果他将自己在资源服务器上的用户名和密码告诉那个网站的话,首先是不安全,那个网站可以随时访问他的资源服务器,其次是权限太大,第三方网站不仅仅可以访问他的照片,还能够访问他的视频和文档,这就很危险了.为了解决这类问题,OAuth就诞生了.他允许用户进行部分授权.他的原理很简单.

OAuth在资源服务器和第三方网站之间做了一个授权层,然后授权层负责针对客户端进行权限的限制,包括权限的范围,有效期,这样客户端因为无法直接访问资源服务器,从而保护了用户的资源.

那么他的运行流程呢,就是这样的:

用户打开客户端以后,客户端要求用户给予授权。

用户同意给予客户端授权。

客户端使用上一步获得的授权,向认证服务器申请令牌。

认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

客户端使用令牌,向资源服务器申请获取资源。

资源服务器确认令牌无误,同意向客户端开放资源。

2.1 OAuth2.0授权模式

在以上这6步中,最关键的一步就是2,用户如何才能够给客户端授权呢?OAuth2.0提供了四中授权方式,分别为:

授权码模式

简化模式

密码模式

客户端模式

2.1.1授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。他的具体步骤如下:

用户访问客户端,后者将前者导向认证服务器。

用户选择是否给予客户端授权。

假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

2.1.2 简化模式

简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。步骤如下:

客户端将用户导向认证服务器。

用户决定是否给于客户端授权。

假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。

浏览器执行上一步获得的脚本,提取出令牌。

浏览器将令牌发给客户端。

2.1.3 密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。步骤如下:

用户向客户端提供用户名和密码。

客户端将用户名和密码发给认证服务器,向后者请求令牌。

认证服务器确认无误后,向客户端提供访问令牌。

2.1.4 客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。步骤如下:

客户端向认证服务器进行身份认证,并要求一个访问令牌。

认证服务器确认无误后,向客户端提供访问令牌。

2.2 SpringBoot中的OAuth2.0

现在我们知道OAuth授权主要由三部分构成,首先是客户端,然后是认证服务器,最后是我们的资源服务器,之前SpringBoot一直将OAuth2.0认证放在了SpringSecurity下,但在SpringBoot2.0文档中,Spring官方说不再提供认证服务器,但是之前的仍然是可以使用的,只是在将来会被移除.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容