Spring整合Spring Security OAuth2.0认证授权

一、基本概念

1.1 什么是认证

比如:在初次使用微信前需要注册成为微信用户,然后输入账号和密码即可登录微信,输入账号和密码
登录微信的过程就是认证。

系统为什么要认证?
认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

认证 :用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信
息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手
机短信登录,指纹认证
等方式。

1.2 什么是会话

用户认证通过后,为了避免用户重复认证,可将用户的信息保证在会话中。会话就是系统为了保持当前
用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

基于session的认证方式如下图:
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的
sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数
据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

Spring整合Spring Security OAuth2.0认证授权_第1张图片

基于token方式如下图:
它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage
等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。

Spring整合Spring Security OAuth2.0认证授权_第2张图片

基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持
cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式,所以基于token的方式更适合。

1.3 什么是授权

授权: 授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有
权限则拒绝访问。

1.4 授权的数据模型

授权可简单理解为Who对What(which)进行How操作

Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。
What,即资源(Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息都属于实体资源(数据资源)实体资源由资源类型和资源实例组成比如商品信息为资源类型,商品编号 为001的商品为资源实例。
How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户对哪些资源都有哪些操作许可。

主体、资源、权限关系如下图:
Spring整合Spring Security OAuth2.0认证授权_第3张图片
主体、资源、权限相关的数据模型如下:
主体(用户id、账号、密码、…)
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
角色(角色id、角色名称、…)
角色和权限关系(角色id、权限id、…)
主体(用户)和角色关系(用户id、角色id、…)

主体(用户)、资源、权限关系如下图:
Spring整合Spring Security OAuth2.0认证授权_第4张图片
通常企业开发中将资源和权限表合并为一张权限表,如下:
资源(资源id、资源名称、访问地址、…)
权限(权限id、权限标识、权限名称、资源id、…)
合并为:
权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)

修改后数据模型之间的关系如下图:

Spring整合Spring Security OAuth2.0认证授权_第5张图片

1.5 RBAC

如何实现授权?业界通常基于RBAC实现授权。

1.5.1 基于角色的访问控制

RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查
询企业运营报表,查询员工工资信息等,访问控制流程如下:
Spring整合Spring Security OAuth2.0认证授权_第6张图片
根据上图中的判断逻辑,授权代码可表示如下:

if(主体.hasRole("总经理角色id")){
查询工资
}

如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是
总经理或部门经理”,修改代码如下:

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
查询工资
}

根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

1.5.2 基于资源的访问控制

RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须
具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
Spring整合Spring Security OAuth2.0认证授权_第7张图片
根据上图中的判断,授权代码可以表示为:

if(主体.hasPermission("查询工资权限标识")){
查询工资
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改
授权代码,系统可扩展性强。

二、基于Session的认证方式

2.1 创建工程

2.1.1 新建maven工程,引入依赖


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.sungroupId>
    <artifactId>security-springmvcartifactId>
    <version>1.0-SNAPSHOTversion>

    
    <packaging>warpackaging>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webmvcartifactId>
            <version>5.1.5.RELEASEversion>
        dependency>

        <dependency>
            <groupId>javax.servletgroupId>
            <artifactId>javax.servlet-apiartifactId>
            <version>3.0.1version>
            <scope>providedscope>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.8version>
        dependency>
    dependencies>
    <build>
        <finalName>security-springmvcfinalName>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.tomcat.mavengroupId>
                    <artifactId>tomcat7-maven-pluginartifactId>
                    <version>2.2version>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-compiler-pluginartifactId>
                    <configuration>
                        <source>1.8source>
                        <target>1.8target>
                    configuration>
                plugin>
                <plugin>
                    <groupId>org.apache.maven.pluginsgroupId>
                    <artifactId>maven-war-pluginartifactId>
                    <version>3.0.0version>
                    <configuration>
                        <failOnMissingWebXml>falsefailOnMissingWebXml>
                    configuration>
                plugin>
                <plugin>
                    <artifactId>maven-resources-pluginartifactId>
                    <configuration>
                        <encoding>utf-8encoding>
                        <useDefaultDelimiters>trueuseDefaultDelimiters>
                        <resources>
                            <resource>
                                <directory>src/main/resourcesdirectory>
                                <filtering>truefiltering>
                                <includes>
                                    <include>**/*include>
                                includes>
                            resource>
                            <resource>
                                <directory>src/main/javadirectory>
                                <includes>
                                    <include>**/*.xmlinclude>
                                includes>
                            resource>
                        resources>
                    configuration>
                plugin>
            plugins>
        pluginManagement>
    build>
project>

2.1.2 Spring 容器配置

/**
 * 相当于applicationContext.xml
 */
@Configuration
@ComponentScan(basePackages = "com.sun.security.springmvc"
        ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
    //在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

2.1.3 servletContext配置

/**
 * 相当于springmvc.xml
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.sun.security.springmvc"
        ,includeFilters= {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {

    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
}

2.1.4 Spring容器初始化

/**
 *  Spring容器初始化,相当于web.xml
 */
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

2.3 实现认证功能

2.3.1 认证页面

在webapp/WEB-INF/views下定义认证页面login.jsp

<%@ page contentType="text/html;charset=UTF-8" pageEncoding="utf-8" %>
<html>
<head>
    <title>用户登录title>
head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>   码:
    <input type="password" name="password"><br>
    <input type="submit" value="登录">
form>
body>
html>

在WebConfig中新增如下配置,将/直接导向login.jsp页面:

 @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
    }

2.3.2 认证接口

定义认证接口,此接口用于对传来的用户名、密码校验,若成功则返回该用户的详细信息,否则抛出错误异常:

/**
 * 认证服务
 */
public interface AuthenticationService {
    /**
     * 用户认证
     * @param authenticationRequest 用户认证请求,账号和密码
     * @return 认证成功的用户信息
     */
    UserDto authentication(AuthenticationRequest authenticationRequest);
}

实现类

/**
 * 认证的实现类
 */
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
    @Override
    public UserDto authentication(AuthenticationRequest authenticationRequest) {
        //校验参数是否为空
        if(null == authenticationRequest ||
                StringUtils.isEmpty(authenticationRequest.getUsername()) ||
                StringUtils.isEmpty(authenticationRequest.getPassword())){
            throw new RuntimeException("账号或密码为空");
        }
        //根据账号去查询数据库,这里测试程序采用模拟方法
        UserDto userDto = getUserDto(authenticationRequest.getUsername());
        if(null == userDto){
            throw new RuntimeException("查询不到该用户");
        }

        if(!userDto.getPassword().equals(authenticationRequest.getPassword())){
            throw new RuntimeException("账号或密码错误");
        }
        return userDto;
    }
    //模拟用户查询
    public UserDto getUserDto(String username){
        return userMap.get(username);
    }
    //用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443"));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553"));
    }
}

请求头

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticationRequest {
    //认证请求参数,账号、密码。。
    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;
}

返回值

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

登录Controller

/**
 * 登录控制器
 */
@RestController
public class LoginController {
    @Autowired
    AuthenticationService authenticationService;
    @RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest){
        UserDto user = authenticationService.authentication(authenticationRequest);
        return user.getUsername()+"登录成功";
    }
}

2.4.实现会话功能

会话是指用户登入系统后,系统会记住该用户的登录状态,他可以在系统连续操作直到退出系统的过程。

(1)UserDto中定义一个SESSION_USER_KEY,作为Session中存放登录用户信息的key。

 public static final String SESSION_USER_KEY = "_user";

(2)修改LoginController,认证成功后,将用户信息放入当前会话。并增加用户登出方法,登出时将session置为失效。

//登录
    @PostMapping(value = "/login", produces = "text/plain;charset=utf-8")
    public String login(AuthenticationRequest authenticationRequest, HttpSession session){
        UserDto user = authenticationService.authentication(authenticationRequest);
        session.setAttribute(user.SESSION_USER_KEY,user);
        return user.getUsername()+"登录成功";
    }
    @GetMapping(value = "/logout",produces = {"text/plain;charset=UTF-8"})
    public String logout(HttpSession session){
        session.invalidate();
        return "退出成功";
    }

(3)从session中获取用户信息

 //从session中获取用户信息
    @GetMapping(value = "/session/getUser",produces = {"text/plain;charset=UTF-8"})
    public String getUser(HttpSession session){
        String fullname = null;
        Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            fullname = "匿名";
        }else{
            UserDto userDto = (UserDto) object;
            fullname = userDto.getFullname();
        }
        return fullname+"访问资源r1";
    }

2.5.实现授权功能

(1)为了实现这样的功能,我们需要在UserDto里增加权限属性,用于表示该登录用户所拥有的权限,同时修改UserDto的构造方法。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDto {
    public static final String SESSION_USER_KEY = "_user";
    //用户身份信息
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
    /**
     * 用户权限
     */
    private Set<String> authorities;

}

(2)并在AuthenticationServiceImpl中为模拟用户初始化权限,其中张三给了p1权限,李四给了p2权限

//用户信息
    private Map<String,UserDto> userMap = new HashMap<>();
    {
        Set<String> authorities1 = new HashSet<>();
        authorities1.add("p1");
        Set<String> authorities2 = new HashSet<>();
        authorities2.add("p2");
        userMap.put("zhangsan",new UserDto("1010","zhangsan","123","张三","133443",authorities1));
        userMap.put("lisi",new UserDto("1011","lisi","456","李四","144553",authorities2));
    }

(3)在LoginController中增加测试资源2

  //从session中获取用户信息
    @GetMapping(value = "/session/getUser2",produces = {"text/plain;charset=UTF-8"})
    public String getUser2(HttpSession session){
        String fullname = null;
        Object object = session.getAttribute(UserDto.SESSION_USER_KEY);
        if(object == null){
            fullname = "匿名";
        }else{
            UserDto userDto = (UserDto) object;
            fullname = userDto.getFullname();
        }
        return fullname+"访问资源r2";
    }

(4)在interceptor包下定义SimpleAuthenticationInterceptor拦截器,实现授权拦截

1、校验用户是否登录
2、校验用户是否拥有操作权限

/**
 * 拦截器
 */
@Component
public class SimpleAuthenticationInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在这个方法中校验用户请求的url是否在用户的权限范围内
        Object object = request.getSession().getAttribute(UserDto.SESSION_USER_KEY);
        if(null == object){
            //没有认证,提示登录
            writeContent(response,"请登录");
        }
        UserDto userDto = (UserDto) object;
        //请求的url
        String requestURI = request.getRequestURI();
        if(userDto.getAuthorities().contains("p1") && requestURI.contains("/session/getUser1")){
            return true;
        }
        if(userDto.getAuthorities().contains("p2") && requestURI.contains("/session/getUser2")){
            return true;
        }
        writeContent(response,"没有权限,拒绝访问");

        return false;
    }

    //响应信息给客户端
    private void writeContent(HttpServletResponse response, String msg) throws IOException {
        response.setContentType("text/html;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.print(msg);
        writer.close();
    }
}

(5)在WebConfig中配置拦截器

	@Autowired
    SimpleAuthenticationInterceptor simpleAuthenticationInterceptor;
    
     @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(simpleAuthenticationInterceptor).addPathPatterns("/session/**");
    }

三、Spring Security快速上手

3.1.1 创建工程引入依赖

<dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-webartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.securitygroupId>
            <artifactId>spring-security-configartifactId>
            <version>5.1.4.RELEASEversion>
        dependency>

3.1.2 Spring容器配置

/**
 * 相当于applicationContext.xml
 */
@Configuration
@ComponentScan(basePackages = "com.sun.security.springmvc"
        ,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class ApplicationConfig {
    //在此配置除了Controller的其它bean,比如:数据库链接池、事务管理器、业务bean等。
}

3.1.3 Servlet Context配置

/**
 * 相当于springmvc.xml
 */
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.sun.security.springmvc"
        ,includeFilters= {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
    //视频解析器
    @Bean
    public InternalResourceViewResolver viewResolver(){
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/view/");
        viewResolver.setSuffix(".jsp");
        return viewResolver;
    }
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }
}

3.1.4 初始化Spring容器

/**
 *  Spring容器初始化,相当于web.xml
 */
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    //spring容器,相当于加载 applicationContext.xml
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{ApplicationConfig.class, WebSecurityConfig.class};
    }

    //servletContext,相当于加载springmvc.xml
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    //url-mapping
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

3.2 认证

3.2.1 认证页面

springSecurity默认提供认证页面,不需要额外开发。
Spring整合Spring Security OAuth2.0认证授权_第8张图片

3.2.2 安全配置

spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置即可使用。

  1. 在config包下定义WebSecurityConfig,安全配置的内容包括:用户信息、密码编码器、安全拦截机制。
/**
 * 安全配置(spring security)
 */
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //定义用户信息服务(查询用户信息)
    @Bean
    public UserDetailsService userDetailsService(){
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return NoOpPasswordEncoder.getInstance();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/session/r1").hasAuthority("p1")
                .antMatchers("/session/r2").hasAuthority("p2")
                .antMatchers("/session/**").authenticated()//所有/session/**的请求必须认证通过
                .anyRequest().permitAll()//除了/session/**,其他请求可以访问
                .and()
                .formLogin()//运行表单登录
                .successForwardUrl("/login-success");//登录访问接口
    }
}
  1. 加载 WebSecurityConfig
    修改SpringApplicationInitializer的getRootConfigClasses()方法,添加WebSecurityConfig.class:
@Override
protected Class<?>[] getRootConfigClasses() {
	return new Class<?>[] { ApplicationConfig.class, WebSecurityConfig.class};
}

3.2.3 Spring Security初始化

Spring Security初始化,这里有两种情况

  • 若当前环境没有使用Spring或Spring MVC,则需要将 WebSecurityConfig(Spring Security配置类) 传入超
    类,以确保获取配置,并创建spring context。
  • 若当前环境已经使用spring,我们应该在现有的springContext中注册Spring Security(上一步已经做将
    WebSecurityConfig加载至rootcontext),此方法可以什么都不做。

在init包下定义SpringSecurityApplicationInitializer:

public class SpringSecurityApplicationInitializer
        extends AbstractSecurityWebApplicationInitializer {
    public SpringSecurityApplicationInitializer() {
        //super(WebSecurityConfig.class);
    }
}

3.2.4 默认根路径请求

在WebConfig.java中添加默认请求根路径跳转到/login,此url为spring security提供:

@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login");
    }

3.2.5 认证成功页面

在安全配置中,认证成功将跳转到/login-success,代码如下:

 //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/session/r1").hasAuthority("p1")
                .antMatchers("/session/r2").hasAuthority("p2")
                .antMatchers("/session/**").authenticated()//所有/session/**的请求必须认证通过
                .anyRequest().permitAll()//除了/session/**,其他请求可以访问
                .and()
                .formLogin()//运行表单登录
                .successForwardUrl("/login-success");//登录访问接口
    }

spring security支持form表单认证,认证成功后转向/login-success。
在LoginController中定义/login-success:

    @RequestMapping(value = "/login-success",produces = {"text/plain;charset=UTF-8"})
    public String loginSuccess(){
        return " 登录成功";
    }

3.4 授权

在LoginController添加/session/r1或/session/r2

/**
     * 测试资源1
     * @return
     */
    @GetMapping(value = "/session/r1",produces = {"text/plain;charset=UTF-8"})
    public String r1(){
        return " 访问资源1";
    }

    /**
     * 测试资源2
     * @return
     */
    @GetMapping(value = "/session/r2",produces = {"text/plain;charset=UTF-8"})
    public String r2(){
        return " 访问资源2";
    }

在安全配置类WebSecurityConfig.java中配置

//安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/session/r1").hasAuthority("p1")
                .antMatchers("/session/r2").hasAuthority("p2")
                .antMatchers("/session/**").authenticated()//所有/session/**的请求必须认证通过
                .anyRequest().permitAll()//除了/session/**,其他请求可以访问
                .and()
                .formLogin()//运行表单登录
                .successForwardUrl("/login-success");//登录访问接口
    }

你可能感兴趣的:(spring)