• SpringSecurity以及Oauth2(笔记)


    一、简介

    1.1 安全框架

    解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制

    1.2 常用安全框架

    • SpringSecurity:Spring家族的一员,是一个能够基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。
    • Apache Shiro:一个功能强大且易于使用的Java安全框架,提供了认证、授权、加密和会话管理。
    • Sa-Token:轻量级的权限认证框架。

    二、SpringSecurity

    2.1 快速开始

    
    <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>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.6.5version>
        parent>
        <groupId>org.examplegroupId>
        <artifactId>SpringSecuritytestartifactId>
        <version>1.0-SNAPSHOTversion>
    
        <properties>
            <maven.compiler.source>11maven.compiler.source>
            <maven.compiler.target>11maven.compiler.target>
            <java.version>1.8java.version>
        properties>
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintagegroupId>
                        <artifactId>junit-vintage-engineartifactId>
                    exclusion>
                exclusions>
            dependency>
            <dependency>
                <groupId>org.springframework.securitygroupId>
                <artifactId>spring-security-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    准备一个Controller

    package com.yjx23332.security.controller;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.GetMapping;
    
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @Slf4j
    public class LoginController {
        @GetMapping("/test")
        public String login(){
            log.info("执行登陆");
            return "已登录";
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们启动项目访问,会发现需要用户名以及密码
    在这里插入图片描述
    这是SpringSecurity生效后的页面,
    默认
    账户:user
    密码:在控制台可以看到
    在这里插入图片描述
    随后才会跳转至
    在这里插入图片描述

    2.2 Security的部分源码

    package org.springframework.security.core.userdetails;
    
    public interface UserDetailsService {
    	/**
    	* @param 前端传入用户名称
    	*/
        UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    UserDetails

    //
    // Source code recreated from a .class file by IntelliJ IDEA
    // (powered by FernFlower decompiler)
    //
    
    package org.springframework.security.core.userdetails;
    
    import java.io.Serializable;
    import java.util.Collection;
    import org.springframework.security.core.GrantedAuthority;
    
    public interface UserDetails extends Serializable {
    	//返回用户权限,且不能返回空
    	/**
    	 * Returns the authorities granted to the user. Cannot return null.
    	 * @return the authorities, sorted by natural key (never null)
    	 */
    	Collection<? extends GrantedAuthority> getAuthorities();
    
    	/**
    	 * Returns the password used to authenticate the user.
    	 * @return the password
    	 */
    	String getPassword();
    
    	/**
    	 * Returns the username used to authenticate the user. Cannot return
    	 * null.
    	 * @return the username (never null)
    	 */
    	String getUsername();
    	//用户是否过期
    	/**
    	 * Indicates whether the user's account has expired. An expired account cannot be
    	 * authenticated.
    	 * @return true if the user's account is valid (ie non-expired),
    	 * false if no longer valid (ie expired)
    	 */
    	boolean isAccountNonExpired();
    	//用户是否锁定
    	/**
    	 * Indicates whether the user is locked or unlocked. A locked user cannot be
    	 * authenticated.
    	 * @return true if the user is not locked, false otherwise
    	 */
    	boolean isAccountNonLocked();
    	//凭证是否过期
    	/**
    	 * Indicates whether the user's credentials (password) has expired. Expired
    	 * credentials prevent authentication.
    	 * @return true if the user's credentials are valid (ie non-expired),
    	 * false if no longer valid (ie expired)
    	 */
    	boolean isCredentialsNonExpired();
    	//是否可用
    	/**
    	 * Indicates whether the user is enabled or disabled. A disabled user cannot be
    	 * authenticated.
    	 * @return true if the user is enabled, false otherwise
    	 */
    	boolean isEnabled();
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64

    它的实现类是User,注意不要和我们自己的混淆

    public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
    		this(username, password, true, true, true, true, authorities);
    	}
    public User(String username, String password, boolean enabled, boolean accountNonExpired,
    			boolean credentialsNonExpired, boolean accountNonLocked,
    			Collection<? extends GrantedAuthority> authorities) {
    		Assert.isTrue(username != null && !"".equals(username) && password != null,
    				"Cannot pass null or empty values to constructor");
    		this.username = username;
    		this.password = password;
    		this.enabled = enabled;
    		this.accountNonExpired = accountNonExpired;
    		this.credentialsNonExpired = credentialsNonExpired;
    		this.accountNonLocked = accountNonLocked;
    		this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    如果我们想要用自己的账号密码等,我们需要覆盖passwordEncoder接口的实现

    /*
     * Copyright 2011-2016 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      https://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.springframework.security.crypto.password;
    
    /**
     * Service interface for encoding passwords.
     *
     * The preferred implementation is {@code BCryptPasswordEncoder}.
     *
     * @author Keith Donald
     */
    public interface PasswordEncoder {
    	// 加密密码,返回加密后字符串
    	/**
    	 * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
    	 * greater hash combined with an 8-byte or greater randomly generated salt.
    	 */
    	String encode(CharSequence rawPassword);
    
    	//匹配 原始密码(也即是传入的密码) 加密码(数据库读出的) 
    	/**
    	 * Verify the encoded password obtained from storage matches the submitted raw
    	 * password after it too is encoded. Returns true if the passwords match, false if
    	 * they do not. The stored password itself is never decoded.
    	 * @param rawPassword the raw password to encode and match
    	 * @param encodedPassword the encoded password from storage to compare with
    	 * @return true if the raw password, after encoding, matches the encoded password from
    	 * storage
    	 */
    	boolean matches(CharSequence rawPassword, String encodedPassword);
    	//是否可以被二次加密,默认返回false
    	/**
    	 * Returns true if the encoded password should be encoded again for better security,
    	 * else false. The default implementation always returns false.
    	 * @param encodedPassword the encoded password to check
    	 * @return true if the encoded password should be encoded again for better security,
    	 * else false.
    	 */
    	default boolean upgradeEncoding(String encodedPassword) {
    		return false;
    	}
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    官方推荐使用BCryptpassword
    在这里插入图片描述

    2.3 自定义登陆逻辑

    创建Config包,设置自动装配的加密类

    package com.yjx23332.security.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    @Configuration
    public class SecurityConfig {
        @Bean
        public PasswordEncoder getPasswordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    接下来的数据操作均为模拟,我们自己实现UserDetailsService
    为了避免循环依赖,此处我们用懒加载

    package com.yjx23332.security.service.Impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.security.core.authority.AuthorityUtils;
    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.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserDetailsServiceImpl  implements UserDetailsService  {
        @Lazy
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
            //1. 查询数据库判断用户名是否存在,不存在则抛出UsernameNotFoundException异常
            if(!"admin".equals(username)){
                throw new UsernameNotFoundException("用户名不存在");
            }
            //2. 把查询出来的密码(注册时已经加密过得密码)与传入密码比较。此处就不查了,直接赋值
            String password = passwordEncoder.encode("123");
            //使用的是Security的User
            return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    这里放密码是数据库的密码,之后会自动比较,我们不需要关注。

    2.4 自定义登陆页面

    修改config

    package com.yjx23332.security.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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;
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception{
            //表单提交
            /**
             * /loginPage 这里是一个 controller 由它跳转至登陆页面,所有的未登录用户都会被跳转至这里
             * 登陆页面的表单 
    需要与loginProcessingUrl对应,此处为/login * /success 这里是一个 controller 由他重定向至登陆成功页面,为Post请求 * /login 不对应任何 controller 触发UserDetailServiceImpl的执行 * /error 这里是一个 controller 登陆失败,会访问 * 当然里面也放静态或者动态页面,本质只是一个资源定位 */ httpSecurity.formLogin() //当发现是/login时,认为是登陆,必须和表单提交地址一样,执行UserDetailServiceImpl .loginProcessingUrl("/login") .loginPage("/loginPage")//自定义登陆页面 //登陆成功跳转页面,必须是Post请求 .successForwardUrl("/success") //登陆失败 .failureForwardUrl("/error"); //授权认证 httpSecurity.authorizeRequests() //进入login.html、error.html不需要被认证 .antMatchers("/loginPage","/error").permitAll() //所有的请求都必须登陆之后,被访问 .anyRequest().authenticated(); //关闭csrf防护 httpSecurity.csrf().disable(); } @Bean public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    表单参数必须为 username 和 password 两个参数,以为它使用了这两个常量
    在这里插入图片描述

    如果我们不想用它自定义的,

     httpSecurity.formLogin()
     				//设置参数别名
                    .usernameParameter("username")
                    .passwordParameter("password")
                    //当发现是/login时,认为是登陆,必须和表单提交地址一样,执行UserDetailServiceImpl
                    .loginProcessingUrl("/login")
                    .loginPage("/loginPage")//自定义登陆页面
                    //登陆成功跳转页面,必须是Post请求
                    .successForwardUrl("/success")
                    //登陆失败
                    .failureForwardUrl("/error");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.5 AuthenticationSuccessHandler与AuthenticationFailureHandler

    如果我们想要跳转至其他网站,我们可以需要先注释掉相关配置,在配置新的处理器
    还需要重写AuthenticationSuccessHandler与AuthenticationFailureHandler

    AuthenticationSuccessHandler

     httpSecurity.formLogin()
     				//设置参数别名
                    .usernameParameter("username")
                    .passwordParameter("password")
                    //当发现是/login时,认为是登陆,必须和表单提交地址一样,执行UserDetailServiceImpl
                    .loginProcessingUrl("/login")
                    .loginPage("/loginPage")//自定义登陆页面
                    //登陆成功跳转页面,必须是Post请求
                    .successHandler(new MyAuthenticationHandler("http://www.baidu.com"));
                    //.successForwardUrl("/success")
                    //登陆失败
                    //.failureForwardUrl("/error");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    重写该方法

    package com.yjx23332.security.handler;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class MyAuthenticationHandler implements AuthenticationSuccessHandler {
        private String url;
        public MyAuthenticationHandler(String url){
            this.url = url;
        }
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        	// User user = (User)authentication.getPrincipal();
            // 安全原因,该方式不会获取到密码,是Null
            // user.getPassword();
            response.sendRedirect(url);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    AuthenticationFailureHandler

    package com.yjx23332.springsecurity.handler;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    public class MyAuthenticationFailurehandler implements AuthenticationFailureHandler {
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setHeader("Content-Type","application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":\"login failed\"}");
            writer.flush();
            writer.close();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.6 anyRequest

    用于所有请求的统一设置,放在最后起到类似else或者default的作用,对前面没有匹配到的进行处理。
    将最不具体的路径(如anyRequest())放在最后面。如果不这样做,那不具体的路径配置将会覆盖掉更为具体的路径配置。

    2.7 antMatchers

    • ?:匹配一个字符
    • *:匹配0个或多个字符
    • **:匹配0个或多个目录
    .antMatchers(HttpMethod.GET,"").permitAll()
    .antMatchers(HttpMethod.POST,"").denyAll()
    
    • 1
    • 2

    2.8 regexMatchers

    regex匹配格式,可以参考
    regex 正则表达式

    .regexMatchers(HttpMethod.DELETE,"^delete").permitAll()
    .regexMatchers(HttpMethod.POST,"").denyAll()
    
    • 1
    • 2

    2.9 mvcMatchers

    针对于全局前缀
    比如我们配置如下

    spring:
      mvc:
        servlet:
          path: /security
    
    • 1
    • 2
    • 3
    • 4

    随后我们访问时就需要
    localhost:8080/security/test

    针对这个,我们需要配置

    .mvcMatchers("/login").servletPath("/security").permitAll()
    
    • 1

    当然我们也可以

    .antMatchers(HttpMethod.GET,"/security/login").permitAll()
    
    • 1

    2.10 权限判定

    	
    	static final String permitAll = "permitAll";
    
    	private static final String denyAll = "denyAll";
    	//可以匿名访问,会进入到拦截链中
    	private static final String anonymous = "anonymous";
    	//认证
    	private static final String authenticated = "authenticated";
    	//完全认证
    	private static final String fullyAuthenticated = "fullyAuthenticated";
    	//记住我,勾选了记住我的用户可访问
    	private static final String rememberMe = "rememberMe";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.10.1 权限

    决定了权限,严格区分大小写

    .antMatchers("").hasAuthority("admin")
    
    //指定多个,只要有一个就可访问
    .antMatchers("").hasAnyAuthority()
    
    • 1
    • 2
    • 3
    • 4

    2.10.2 角色

    角色格式:
    ROLE_XXX

    new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_abc"));
    
    • 1
    .antMatchers("/login").hasRole("abc")
    .antMatchers("/login").hasAnyRole("abc")
    
    • 1
    • 2

    也是严格区分大小写

    2.10.3 IP地址

    获取

    request.getRemoteAddr();
    
    • 1

    2.11 访问异常处理

    package com.yjx23332.security.handler;
    
    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;
    
    @Component
    public class MyAccessDeniedhandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setHeader("Content-type","application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":\"access denied\"}");
            writer.flush();
            writer.close();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        MyAccessDeniedhandler myAccessDeniedhandler;
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception{
            //表单提交
            httpSecurity.formLogin()
                    .loginPage("loginPage")
                    .loginProcessingUrl("/login")
                    .successHandler(new MyAuthenticationSuccessHandler())
                    .failureHandler(new MyAuthenticationFailurehandler());
            //授权认证
            httpSecurity.authorizeRequests()
                    .antMatchers("/loginPage").permitAll()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().authenticated();
            //关闭csrf防护
            httpSecurity.csrf().disable();
    
    
            //异常处理
            httpSecurity.exceptionHandling()
                    .accessDeniedHandler(myAccessDeniedhandler);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.12 access自定义权限控制

    旧版本

    AuthenticationService 是自己创建的接口。

    package com.yjx23332.security.service.Impl;
    
    import com.yjx23332.security.service.AuthenticationService;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import javax.servlet.http.HttpServletRequest;
    import java.util.Collection;
    
    @Service
    public class AuthenticationServiceImpl implements AuthenticationService {
        @Override
        public boolean hasPermision(HttpServletRequest request, Authentication authentication) {
            Object object = authentication.getPrincipal();
            if(object instanceof UserDetails){
                UserDetails userDetails = (UserDetails)  object;
                Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
                //查看是否含有该URI的权限
                return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
            }
            return false;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        MyAccessDeniedhandler myAccessDeniedhandler;
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception{
            //表单提交
            httpSecurity.formLogin()
                    .loginPage("loginPage")
                    .loginProcessingUrl("/login")
                    .successHandler(new MyAuthenticationSuccessHandler())
                    .failureHandler(new MyAuthenticationFailurehandler());
            //授权认证
            httpSecurity.authorizeRequests()
                    .antMatchers("/loginPage").permitAll()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().access("@authenticationServiceImpl.hasPermision(HttpServletRequest request, Authentication authentication)");
            //关闭csrf防护
            httpSecurity.csrf().disable();
    
    
            //异常处理
            httpSecurity.exceptionHandling()
                    .accessDeniedHandler(myAccessDeniedhandler);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    新版本

    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        MyAccessDeniedhandler myAccessDeniedhandler;
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception{
            //表单提交
            httpSecurity.formLogin()
                    .loginPage("loginPage")
                    .loginProcessingUrl("/login")
                    .successHandler(new MyAuthenticationSuccessHandler())
                    .failureHandler(new MyAuthenticationFailurehandler());
            //授权认证
            httpSecurity.authorizeRequests()
                    .antMatchers("/loginPage").permitAll()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
                        Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
                        HttpServletRequest request = requestAuthorizationContext.getRequest();
                        //查看是否含有该URI的权限,有则返回true
                        return new AuthorizationDecision(
                                authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())
                                )
                        );
                    });
            //关闭csrf防护
            httpSecurity.csrf().disable();
    
    
            //异常处理
            httpSecurity.exceptionHandling()
                    .accessDeniedHandler(myAccessDeniedhandler);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    2.13 基于注解的访问控制

    2.13.1 @Secured

    判断是否具有角色。能写在方法或者类上,Controller和Service上,参数仍然要以ROLE_开头。严格区分大小写。
    默认不可用,需要

    package com.yjx23332.security;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    
    
    @SpringBootApplication
    @EnableGlobalMethodSecurity(securedEnabled = true)
    public class MainApplication {
        public static void main(String[] args){
            SpringApplication.run(MainApplication.class,args);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    配置类中的角色全部注释掉

    @Secured("ROLE_abc")
    @RequestMapping("test")
    public String toMain(){ return "test" }
    
    • 1
    • 2
    • 3

    2.13.2 @PreAuthorize/@PostAuthorize

    方法执行前判断与方法执行后判断。写在方法或者类上
    同理也要开启

    package com.yjx23332.security;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    
    
    @SpringBootApplication
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class MainApplication {
        public static void main(String[] args){
            SpringApplication.run(MainApplication.class,args);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    写法与旧版access一致,但是此处也可以用ROLE_开头。严格区分大小写。

    @PreAuthorize("hasRole('abc')")
    @RequestMapping("test")
    public String toMain(){ return "test" }
    
    • 1
    • 2
    • 3

    2.14 Remember-Me

    用户只需要在登陆时添加remember-me复选框,取值为true。SpringSecurity会自动把用户存储到数据院中,以后就可以不登陆进行访问。

    需要配置mybatis,此处省略。

    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        MyAccessDeniedhandler myAccessDeniedhandler;
        @Autowired
        UserDetailsService userDetailsService;
        @Autowired
        DataSource dataSource;
    
        @Autowired
        PersistentTokenRepository persistentTokenRepository;
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception{
            //表单提交
            httpSecurity.formLogin()
                    .loginPage("loginPage")
                    .loginProcessingUrl("/login")
                    .successHandler(new MyAuthenticationSuccessHandler())
                    .failureHandler(new MyAuthenticationFailurehandler());
            //授权认证
            httpSecurity.authorizeHttpRequests()
                    .antMatchers("/loginPage").permitAll()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
                        Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
                        HttpServletRequest request = requestAuthorizationContext.getRequest();
                        //查看是否含有该URI的权限,有则返回true
                        return new AuthorizationDecision(
                                authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())
                                )
                        );
                    });
            //关闭csrf防护
            httpSecurity.csrf().disable();
            //记住我
            httpSecurity.rememberMe()
                    //设置失效时间,默认2周
                    .tokenValiditySeconds(60)
                    //修改remember-me对应的参数名
                    .rememberMeParameter("")
                    //domain域
                    .rememberMeCookieDomain("")
                    //Cookie名字
                    .rememberMeCookieName("")
                    //自定义登陆逻辑
                    .userDetailsService(userDetailsService)
                    //持久层对象
                            .tokenRepository(persistentTokenRepository);
    
            //异常处理
            httpSecurity.exceptionHandling()
                    .accessDeniedHandler(myAccessDeniedhandler);
        }
        @Bean
        public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); }
    
        @Bean
        public PersistentTokenRepository getPersistentTokenRepository(){
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            //自动建表,第一次启动时需要,第二次启动需要注释掉
            jdbcTokenRepository.setCreateTableOnStartup(true);
            return jdbcTokenRepository;
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    2.15 退出登陆

    直接访问即可

    /logout

    也可以,设置退出登录的位置等信息

    httpSecurity.logout()
    			//设置前端退出的URI
    				.logoutUrl("/logout")
    			//设置退出跳转页面
    	            .logoutSuccessUrl("");
    
    • 1
    • 2
    • 3
    • 4
    • 5

    2.16 csrf 防护

    CSRF(Cross-site request forgery)跨站请求伪造,通过伪造用户请求访问收信人站点的非法请求访问。
    不法分子通过劫持Cookie中SessionID来让服务器认为对方就是客户。
    在跨域的情况下,sessionID可能被恶意劫持。

    SpringSecurity4开始默认开启,会默认拦截请求,进行CSRF处理。为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务器产生)的内容,如果token和服务器token匹配成功,则正常访问。

    我们之前一致都是关闭状态,开启后我们就需要增加一个获取服务端token的过程。

    我们以thymeleaf为例。
    前端需要先获取一个服务器生成码,随后再发送。增加如下内容即可。

    DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
    	  xmlns:th="http://www.thymeleaf.org">
    <head>
    	<meta charset="UTF-8">
    	<title>Titletitle>
    head>
    <body>
    <form action="/login" method="post">
    	<intput type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
    	用户名:<input type="text" name="username"/><br/>
    	密码:<input type="text" name="password"/><br/>
    	记住我:<input type="checkbox" name="remember-me" value="true" /><br/>
    	<input type="submit" value="登陆"/>
    <form>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    2.17 前后端分离模式

    我们将上述的过程整合一下,如下完成前后端模式

    2.17.1 配置

    package com.yjx23332.security.config;
    
    
    import com.yjx23332.security.handler.*;
    import com.yjx23332.security.service.Impl.UserDetailsServiceImpl;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    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.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    
    
    
    @EnableWebSecurity
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        UserDetailsService userDetailsService;
    
        /**
         * 绑定新的认证处理方法
         * */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService);
        }
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
    
            //表单提交
            httpSecurity.formLogin()
                    .permitAll()
                    .successHandler(new MyAuthenticationSuccessHandler("https://baidu.com"))
                    .failureHandler(new MyAuthenticationFailurehandler());
            //授权认证
            httpSecurity.authorizeRequests()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().authenticated();
            //异常处理
            httpSecurity.exceptionHandling()
                    .accessDeniedHandler(new MyAccessDeniedhandler())
                    .authenticationEntryPoint(new MyAuthenticationEntryPoint());
            //登出处理
            httpSecurity.logout()
                    .permitAll()
                    .logoutSuccessHandler(new MyLogoutSuccessHandler())
                    .deleteCookies();
            //限制账户登录数目
            httpSecurity.sessionManagement().
                    maximumSessions(1)
                    .expiredSessionStrategy(new MySessionInformationExpiredStrategy());
            //csrf防御关闭
            httpSecurity.csrf().disable();
            //允许跨域
            httpSecurity.cors().disable();
        }
        @Bean
        public PasswordEncoder getPasswordEncoder(){
            return new BCryptPasswordEncoder();
        }
        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77

    2.17.2 拒绝访问配置

    package com.yjx23332.springsecurity.handler;
    
    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;
    
    
    
    public class MyAccessDeniedhandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setHeader("Content-type","application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":\"access denied\"}");
            writer.flush();
            writer.close();
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2.17.3 匿名拒绝

    package com.yjx23332.springsecurity.handler;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    
    public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, IOException {
            httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
            httpServletResponse.setContentType("text/json;charset=utf-8");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":\"annoymous refused\"}");
            writer.flush();
            writer.close();
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.17.4 失败处理

    package com.yjx23332.springsecurity.handler;
    
    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.io.PrintWriter;
    
    
    public class MyAuthenticationFailurehandler implements AuthenticationFailureHandler {
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setHeader("Content-Type","application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":"+exception.getStackTrace()+"}");
            writer.flush();
            writer.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.17.5 成功处理

    package com.yjx23332.springsecurity.handler;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.userdetails.User;
    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;
    
    
    @Slf4j
    public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        private String url;
        public MyAuthenticationSuccessHandler(String url){
            this.url = url;
        }
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        	User user = (User)authentication.getPrincipal();
            // 安全原因,该方式不会获取到密码,是Null
            log.info("密码:{}",user.getPassword());
            log.info("账户:{}",user.getUsername());
            log.info("权限:{}",user.getAuthorities());
            response.sendRedirect(url);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    2.17.6 成功注销处理

    package com.yjx23332.springsecurity.handler;
    
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    
    public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
            httpServletResponse.setStatus(HttpServletResponse.SC_OK);
            httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":\"logout success\"}");
            writer.flush();
            writer.close();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    2.17.7 连接失效策略

    package com.yjx23332.springsecurity.handler;
    
    
    import org.springframework.security.web.session.SessionInformationExpiredEvent;
    import org.springframework.security.web.session.SessionInformationExpiredStrategy;
    
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.io.PrintWriter;
    
    public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException {
            HttpServletResponse httpServletResponse = sessionInformationExpiredEvent.getResponse();
            httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
            httpServletResponse.setHeader("Content-type","application/json;charset=utf-8");
            PrintWriter writer = httpServletResponse.getWriter();
            writer.write("{\"status\":\"error\",\"msg\":\"connection invalid\"}");
            writer.flush();
            writer.close();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    2.17.8 修改登陆逻辑

    package com.yjx23332.security.service.Impl;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.security.core.authority.AuthorityUtils;
    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.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserDetailsServiceImpl  implements UserDetailsService  {
        @Lazy
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
            //1. 查询数据库判断用户名是否存在,不存在则抛出UsernameNotFoundException异常
            if(!"admin".equals(username)){
                throw new UsernameNotFoundException("用户名不存在");
            }
            //2. 把查询出来的密码(注册时已经加密过得密码)与传入密码比较。此处就不查了,直接赋值
            String password = passwordEncoder.encode("123");
            //使用的是Security的User
            return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这里插入图片描述

    2.18 流程图解

    图片源自互联网
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    三、SpringSecurity Oauth2

    3.1 Oauth2简介

    我们登陆的时候可以用微信、QQ的方式进行一键登陆,并且不存在则进行绑定。

    不同网站代码、认证标准不同。因此Oauth2主要解决的就是通用标准的问题,实现跨站系统认证,各系统之间要遵循一定的接口协议。

    3.2 流程

    用户 网站 微信认证 微信用户 访问登陆页面 请求认证 授权页面 用户授权 授权码 请求获取用户信息 校验令牌合法性 响应用户信息 显示用户信息 用户 网站 微信认证 微信用户
    1.请求授权AuthorizationRequest
    2.授权许可AuthorizationGrant
    3.授权许可AuthorizationGrant
    4.访问令牌AccessToken
    5.访问令牌AccessToken
    6.受保护资源ProtectedResource
    客户端
    资源拥有者
    验证服务器
    资源服务器

    3.3 常用术语

    • 客户凭证clientCredentials:客户端的clientId和密码用于认证用户
    • 令牌tokens:授权服务器在接收到客户端请求后,颁发的访问令牌
    • 作用域:客户请求访问令牌时,有资源拥有者额外指定的细分权限(permission)

    3.4 令牌类型

    • 授权码:仅用于授权码授权类型,用于交换机获取令牌和刷新令牌
    • 访问令牌:用于代表一个用户或服务直接去访问受保护的资源
    • 刷新令牌:用于去授权服务器获取一个新的访问令牌
    • BearerToken:不管谁拿到Token都可以访问资源,类似现金
    • Proof of Possesion(PoP) token:可以校验client是否对Token有明确的拥有权

    3.5 特点

    优点:

    • 更安全,客户端不接触用户密码,服务器端更易集中保护
    • 广泛传播并被持续采用
    • 短寿命和封装的token
    • 资源服务器和授权服务器解耦
    • 集中式授权,简化客户端
    • HTTP/JSON友好,易于请求和传递token
    • 考虑多种客户端架构场景
    • 客户可以具有不同的级别

    缺点:

    • 协议框架太宽泛,造成各种实现的兼容性和互操作性差
    • 不是一个认证协议,本身并不能告诉你任何用户信息

    3.6 授权模式

    3.6.1 授权码模式(Authorization Code)

    客户端 代理 授权服务器 资源服务器 访问登陆页面 请求认证 授权页面 用户授权 授权码 授权码 访问令牌和可选刷新令牌 请求获取用户信息 校验令牌合法性 响应用户信息 显示用户信息 客户端 代理 授权服务器 资源服务器

    3.6.2 简化授权模式(Implicit)

    客户端 代理 授权服务器 后台客户端资源 资源服务器 访问登陆页面 请求认证 授权页面 用户授权 重定向URI以及访问令牌(在Fragmen中t) 利用重定向URI到达 Script 使用脚本解析,获取令牌 请求获取用户信息 校验令牌合法性 响应用户信息 显示用户信息 客户端 代理 授权服务器 后台客户端资源 资源服务器

    3.6.3 密码模式(Resource Owner PasswordCredentials)

    客户端 代理 授权服务器 资源服务器 访问登陆页面 用户密码验证 访问令牌和可选刷新令牌 请求获取用户信息 校验令牌合法性 响应用户信息 显示用户信息 客户端 代理 授权服务器 资源服务器

    3.6.4 客户端模式(Client Credentials)

    客户端 授权服务器 资源服务器 客户端授权 访问令牌 请求获取用户信息 校验令牌合法性 响应用户信息 显示用户信息 客户端 授权服务器 资源服务器

    3.6.5 刷新令牌

    客户端 授权服务器 资源服务器 授权信息 访问令牌和刷新令牌 访问令牌 校验令牌合法性 受保护的资源 访问令牌 校验令牌合法性 无效Token错误 刷新令牌 访问令牌和可选刷新令牌 访问令牌 校验令牌合法性 受保护的资源 显示用户信息 客户端 授权服务器 资源服务器

    3.7 SpringSecurity Oauth2

    3.7.1 授权服务器

    授权服务器

    • Authorize Endpoint(/oauth2/authorize):授权端点,进行授权
    • Token Endpoint(/oauth2/token):令牌端点,进过授权拿到对应的Token
    • Introspection Endpoint(/oauth2/introspect):检验端点,检验Token的合法性
    • Revocation Endpoint(/oauth2/revoke):撤销端点,撤销授权

    3.7.2 架构

    图片来源自互联网

    在这里插入图片描述

    3.7.3 授权码模式

    
    <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>
        <parent>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-parentartifactId>
            <version>2.6.5version>
        parent>
        <groupId>org.examplegroupId>
        <artifactId>SpringSecuritytestartifactId>
        <version>1.0-SNAPSHOTversion>
    
        <properties>
            <maven.compiler.source>11maven.compiler.source>
            <maven.compiler.target>11maven.compiler.target>
            <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
            <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
            <java.version>11.0.2java.version>
            <spring-cloud.version>2021.1spring-cloud.version>
    
        properties>
        <dependencyManagement>
            <dependencies>
                
                <dependency>
                    <groupId>org.springframework.cloudgroupId>
                    <artifactId>spring-cloud-dependenciesartifactId>
                    <version>${spring-cloud.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-securityartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-starter-oauth2artifactId>
            dependency>
    
            
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
    
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
                <scope>testscope>
                <exclusions>
                    <exclusion>
                        <groupId>org.junit.vintagegroupId>
                        <artifactId>junit-vintage-engineartifactId>
                    exclusion>
                exclusions>
            dependency>
            <dependency>
                <groupId>org.springframework.securitygroupId>
                <artifactId>spring-security-testartifactId>
                <scope>testscope>
            dependency>
        dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                plugin>
            plugins>
        build>
    project>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84

    使用自定义的User,相关部分替换即可

    package com.yjx23332.security.entity.dto;
    
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.Collection;
    import java.util.List;
    
    public class User implements UserDetails {
        private String username;
        private String password;
        private List<GrantedAuthority> authorityList;
    
        public User(String username,String password,List<GrantedAuthority> authorities){
            this.username = username;
            this.password = password;
            this.authorityList = authorities;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return authorityList;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return true;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return true;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    认证服务器配置

    package com.yjx23332.security.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    
    @Configuration
    @EnableAuthorizationServer
    /**
     * 授权服务器
     * */
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private PasswordEncoder passwordEncoder;
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    //配置Client-ID
                    .withClient("admin")
                    //配置Client-secret
                    .secret(passwordEncoder.encode("112233"))
                    //访问token有效期
                    .accessTokenValiditySeconds(3600)
                    //授权成功后跳转
                    .redirectUris("http://www.baidu.com")
                    //使用范围
                    .scopes("all")
                    //授权码模式
                    .authorizedGrantTypes("authorization_code");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    资源服务器配置

    package com.yjx23332.security.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    
    /**
     * 资源服务器配置
     * */
    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    //所有资源都需要被认证
                    .anyRequest()
                    .authenticated()
                .and()
                    //有权限的用户可访问
                    .requestMatchers()
                    .antMatchers("/user/**");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    security配置

    package com.yjx23332.security.config;
    
    
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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;
    
    
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
    
            //表单提交
            httpSecurity.formLogin()
                    .permitAll();
            //授权认证
            httpSecurity.authorizeRequests()
                    .antMatchers("/oauth/**").permitAll()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().authenticated();
            //登出处理
            httpSecurity.logout()
                    .permitAll();
            //csrf防御关闭
            httpSecurity.csrf().disable();
            //允许跨域
            httpSecurity.cors().disable();
        }
        @Bean
        public PasswordEncoder getPasswordEncoder(){
            return new BCryptPasswordEncoder();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    controller配置
    资源服务接口

    package com.yjx23332.security.controller;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
        /**
         * 获取当前用户
         * */
        @RequestMapping("/getCurrentUser")
        public Object getCurrentUser(Authentication authentication){
            return authentication.getPrincipal();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    访问页面为

    localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_url=http://www.baidu.com&scope=all

    登陆后跳转至如下默认页面

    在这里插入图片描述
    上方就为授权码
    在这里插入图片描述

    接下来用Post的方式,去获取授信息
    笔者此处使用PostMan
    首先我们要填写如下信息,右侧username和password是客户端的内容,我们已经在配置校验的时候已经配好了
    在这里插入图片描述
    我们接下俩配置如下内容后(也与我们配置的校验服务器一致),发送
    在这里插入图片描述

    通过令牌获取
    在这里插入图片描述

    如果想要替换授权码页面怎么做?可以参考自定义OAuth2授权同意页面

    3.7.4 密码模式

    package com.yjx23332.security.config;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    
    @Configuration
    @EnableAuthorizationServer
    /**
     * 授权服务器
     * */
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private PasswordEncoder passwordEncoder;
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                    //配置Client-ID
                    .withClient("admin")
                    //配置Client-secret
                    .secret(passwordEncoder.encode("112233"))
                    //访问token有效期
                    .accessTokenValiditySeconds(3600)
                    //授权成功后跳转
                    .redirectUris("http://www.baidu.com")
                    //使用范围
                    .scopes("all")
                    //授权码模式
                    .authorizedGrantTypes("password");
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    package com.yjx23332.security.config;
    
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    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;
    
    
    
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
    
            //表单提交
            httpSecurity.formLogin()
                    .permitAll();
            //授权认证
            httpSecurity.authorizeRequests()
                    .antMatchers("/oauth/**").permitAll()
                    //所有的请求都必须登陆之后,被访问
                    .anyRequest().authenticated();
            //登出处理
            httpSecurity.logout()
                    .permitAll();
            //csrf防御关闭
            httpSecurity.csrf().disable();
            //允许跨域
            httpSecurity.cors().disable();
        }
        @Bean
        public PasswordEncoder getPasswordEncoder(){
            return new BCryptPasswordEncoder();
        }
    	
    	@Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    我们将获取令牌改为如下内容后发送,Authorization不用修改
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    网络安全(黑客)—小白自学
    【安装笔记-20240529-Windows-Electerm 终端工具】
    19. 删除链表的倒数第 N 个结点
    codeforces 828 E1
    嵌入式Linux C进程间通信——IPC概述和信号
    C++核心编程--类篇
    C++--哈希思想的应用--位图--布隆过滤器的介绍--1112
    CNN(卷积神经网络)、RNN(循环神经网络)和GCN(图卷积神经网络)
    软件测试工程师成长记:职场人的职业探寻之路
    Maven项目管理(一)
  • 原文地址:https://blog.csdn.net/weixin_46949627/article/details/126698801