Spring Security

Spring Security和Shiro

框架名称 特点 应用 常用组合
Spring Security 功能更丰富,社区资源丰富 中大型的项目 Spring Boot/Spring Cloud + Spring Security
Shiro 上手更加的简单 小项目 SSM + Shiro

认证和授权

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

在Spring Security中就具体化为用户认证(Authentication)和用户授权(Authorization)两个部分。

用户认证

验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。

用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。

通俗的说就是系统认为用户是否能登录。

用户授权

验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。

比如对某一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。

一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

通俗点讲就是系统判断用户是否有权限去做某些事情

SpringBoot整合SpringSecurity

image-20230726092135439

image-20230729095324464

启动项目,控制台中会打印出来密码:

image-20230726092246996

我们在浏览器中输入http://localhost:8080/user/list

直接跳转到http://localhost:8080/login

image-20230726092310004

输入完毕后点击Sign in 说明登录成功 跳转到http://localhost:8080/user/list

运行出结果

修改用户名和密码

spring.security.user.name=root
spring.security.user.password=123456

当前用户登录后,有没有权限去访问相应的请求

授权

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.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/*开启安全管理配置*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

     @Override//自定义认证
    protected void configure(HttpSecurity http) throws Exception {
        //authorizeRequests项目中的所有controller请求
        //antMatchers不需要认证
        //.permitAll();任何人都可以所以访问
        http.authorizeRequests().antMatchers("/login").permitAll();
        //验证角色商家可以发布商品
        //http.authorizeRequests().antMatchers("/admin/pushGoods").hasRole("shangjia");
        //验证有发布商品权限的所以用户
        http.authorizeRequests().antMatchers("/admin/add","/delete","/pushGoods").hasAuthority("shangjiaquanxian");
        //其他请求需要登录后访问
        http.authorizeRequests().anyRequest().authenticated();
        //目前使用表单登录
        http.formLogin();
    }
}

springsecurity的认证授权流程

Spring Security最核心的东西是一个过滤器链,这些过滤器在Spring boot启动的时候会帮我们配置上。

image-20230726092906557

执行流程

image-20230726111837171

文字描述话术:
具体的执行流程其实是一个过滤链:

username=root&&password=123456

1.UsernamePasswordAuthenticationFilter
拦截 /login post
从这个请求中 根据username 取出了用户名 root 
根据password 取出了密码 123456

Authentication a1 = new Authentication(a1,setUsername("root"))
a1.setPassword("123456)

2.在第一步的方法中
我们继续调用ProviderManager.authenticate(a1 );

3.因为我们是表单提交 所以第三步
ProviderManager.authenticate(a1);
在authenticate内部继续调用DaoAuthenticationProvider.authencate(a1)

4.我们会继续调用InMemoryUserDetailsManager的loadUserByUsername(a1.username)
这个loadUserByUsername方法的返回值中包含 这个用户的用户名 密码 权限 角色信息
这个信息存放在一个叫UserDetails对象 这个对象中有当前登录这个用户的用户名root 密码123456对应的密码 权限角色信息

5.把a1中登录的密码 加密一下和 UserDetails中的密码进行比较 如果一样登录成功否则就是登录失败

6.如果登录成功 我们需要把UserDetails中的权限信息复制一份到a1对象中 返回a1对象

7.把a1对象存放在一个SecurityContext的上下文中?把a1对象存放到session中

1、用户向应用程序发起请求,请求需要经过Spring Security的过滤器链。
2、过滤器链首先会经过UsernamePasswordAuthenticationFilter过滤器,该过滤器判断请求是否是一个认证请求(如何知道是一个认证请求 拦截 对/login 的 POST 请求做拦截,校验表单中用户名,密码)。如果是认证请求,过滤器将获取请求中的用户名和密码,然后使用AuthenticationManager进行身份认证。
3、AuthenticationManager会根据用户名和密码创建一个Authentication对象,并将该对象传递给AuthenticationProvider进行认证。
4、AuthenticationProvider会根据传递过来的Authentication对象进行身份认证,并返回一个认证成功或失败的结果。
5、如果认证成功,UsernamePasswordAuthenticationFilter会将认证信息封装成一个Authentication对象,并将其放入SecurityContextHolder上下文中。

SecurityContextHolder

SecurityContextHolder上下文

session 一个用户在服务器上有一片空间 我们可以向这个空间中存放数据 只要是这个用户发送的请求 都可以共享这个空间的数据

SecurityContextHolder

获取securityContext,SecurityContextHolder的 getContext() 方法

SecurityContext securityContext = SecurityContextHolder.getContext();
1
从securityContext获取Authentication

Authentication authentication = securityContext.getAuthentication()
1
获取用户的信息,也就是UserDetails

UserDetails principal = (UserDetails)authentication.getPrincipal();

SecurityContextHolder 用来获取登录之后用户信息。Spring Security 会将登录用户数据保存在 Session 中。但是,为了使用方便,Spring Security在此基础上还做了一些改进,其中最主要的一个变化就是线程绑定。当用户登录成功后,Spring Security 会将登录成功的用户信息保存到 SecurityContextHolder 中。SecurityContextHolder 中的数据保存默认是通过ThreadLocal 来实现的,使用 ThreadLocal 创建的变量只能被当前线程访问,不能被其他线程访问和修改,也就是用户数据和请求线程绑定在一起。当登录请求处理完毕后,Spring Security 会将 SecurityContextHolder 中的数据拿出来保存到 Session 中,同时将 SecurityContexHolder 中的数据清空。以后每当有请求到来时,Spring Security 就会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,方便在该请求的后续处理过程中使用,同时在请求结束时将 SecurityContextHolder 中的数据拿出来保存到 Session 中,然后将 Security SecurityContextHolder 中的数据清空。这一策略非常方便用户在 Controller、Service 层以及任何代码中获取当前登录用户数据。

6、用户请求获取资源时,会经过FilterSecurityInterceptor过滤器,该过滤器会根据请求的URL和HTTP方法获取访问控制列表(Access Control List)。
7、Access Control List会包含访问资源所需要的权限信息,FilterSecurityInterceptor会将Authentication对象和Access Control List传递给AccessDecisionManager进行授权决策。
8、AccessDecisionManager会调用多个AccessDecisionVoter进行投票,并根据投票结果来决定当前用户是否有访问该资源的权限。如果用户被授权访问资源,应用程序将返回资源的响应结果。

总结就是首先经过认证过滤器实现认证,认证成功的话就会将用户信息存到authentication对象里面放到security上下文去(后续的权限校验需要获取到),这里面是包括权限的,之后再由AccessDecisionManager去根据相关策略进行权限鉴定

UsernamePasswordAuthenticationFilter 在org.springframework.security.web.authentication包下 可以查看源码

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
这也是第一个过滤器
过滤器来了先走这个方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            username = username != null ? username : "";
            username = username.trim();
            String password = this.obtainPassword(request);
            password = password != null ? password : "";
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
  
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);// 将登录成功信息存放在
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }

        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        this.logger.trace("Failed to process authentication request", failed);
        this.logger.trace("Cleared SecurityContextHolder");
        this.logger.trace("Handling authentication failure");
        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }
}

ProviderManager

ProviderManager 在 org.springframework.security.authentication包下 可以查看源码 他实现了AuthenticationManager这个接口

 public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        Iterator var9 = this.getProviders().iterator();

        while(var9.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var9.next();
            if (provider.supports(toTest)) {
                if (logger.isTraceEnabled()) {
                    Log var10000 = logger;
                    String var10002 = provider.getClass().getSimpleName();
                    ++currentPosition;
                    var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
                }

                try {
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (InternalAuthenticationServiceException | AccountStatusException var14) {
                    this.prepareException(var14, authentication);
                    throw var14;
                } catch (AuthenticationException var15) {
                    lastException = var15;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                parentResult = this.parent.authenticate(authentication);
                result = parentResult;
            } catch (ProviderNotFoundException var12) {
            } catch (AuthenticationException var13) {
                parentException = var13;
                lastException = var13;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            if (parentResult == null) {
                this.eventPublisher.publishAuthenticationSuccess(result);
            }

            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            if (parentException == null) {
                this.prepareException((AuthenticationException)lastException, authentication);
            }

            throw lastException;
        }
    }
}

因为是表单提交 所以调用的是DaoAuthenticationProvider

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

 public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
            return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
        });
        String username = this.determineUsername(authentication);
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("Failed to find user '" + username + "'");
                if (!this.hideUserNotFoundExceptions) {
                    throw var6;
                }

                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        this.prepareTimingAttackProtection();

        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
            } else {
                return loadedUser;
            }
        } catch (UsernameNotFoundException var4) {
            this.mitigateAgainstTimingAttack(authentication);
            throw var4;
        } catch (InternalAuthenticationServiceException var5) {
            throw var5;
        } catch (Exception var6) {
            throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
        }
    }


}

核心组件介绍:

Authentication
Authentication是一个接口,用来表示用户认证信息。
该对象主要包含了用户的详细信息(UserDetails)和用户鉴权时所需要的信息,如用户提交的用户名密码、Remember-me Token,或者digest hash值等。按不同鉴权方式使用不同的Authentication实现。
在用户登录认证之前相关信息会封装为一个Authentication具体实现类的对象,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的Authentication对象,然后把它保存在 SecurityContextHolder所持有的SecurityContext中,供后续的程序进行调用,如访问权限的鉴定等。

接口中的方法:
从这个接口中,我们可以得到用户身份信息,密码,细节信息,认证信息,以及权限列表,具体的详细解读如下:

getAuthorities(): 用户权限信息(权限列表),通常是代表权限的字符串列表;

getCredentials(): 用户认证信息(密码信息),由用户输入的密码凭证,认证之后会移出,来保证安全性;

getDetails(): 细节信息,Web应用中一般是访问者的ip地址和sessionId;

getPrincipal(): 用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等);

isAuthenticated: 获取当前 Authentication 是否已认证;

setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。

官方文档里说过,当用户提交登陆信息时,会将用户名和密码进行组合成一个实例UsernamePasswordAuthenticationToken,而这个类是Authentication的一个常用的实现类,用来进行用户名和密码的认证,类似的还有RememberMeAuthenticationToken,它用于记住我功能。

GrantedAuthority
Authentication的getAuthorities()方法返回一个 GrantedAuthority 对象数组。

GrantedAuthority该接口表示了当前用户所拥有的权限(或者角色)信息,用于配置 web授权、方法授权、域对象授权等。该属性通常由UserDetailsService 加载给 UserDetails。这些信息由授权负责对象AccessDecisionManager来使用,并决定最终用户是否可以访问某资源(URL或方法调用或域对象)。鉴权时并不会使用到该对象。
如果一个用户有几千个这种权限,内存的消耗将会是非常巨大的。

5.UserDetails
UserDetails存储的就是用户信息,它和Authentication接口类似,都包含了用户名,密码以及权限信息。

而区别就是Authentication中的getCredentials来源于用户提交的密码凭证,而UserDetails中的getPassword取到的则是用户正确的密码信息,认证的第一步就是比较两者是否相同,除此之外,Authentication#getAuthorities是认证用户名和密码成功之后,由UserDetails#getAuthorities传递而来。而Authentication中的getDetails信息是经过了AuthenticationProvider认证之后填充的。

其接口方法含义如下:

getAuthorites:获取用户权限,本质上是用户的角色信息。

getPassword: 获取密码。

getUserName: 获取用户名。

isAccountNonExpired: 账户是否过期。

isAccountNonLocked: 账户是否被锁定。

isCredentialsNonExpired: 密码是否过期。

isEnabled: 账户是否可用。

完成自定义的认证授权流程

1.UsernamePasswordAuthenticationFilter这个过滤器如何知道我们提交的是登录操作,

2.去数据库验证用户名和密码的操作应该写在什么地方?

我们需要进行自定义配置

package com.tyhxzy.springsecurity.config;

import org.springframework.context.annotation.Bean;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

// 开启安全框架
@EnableWebSecurity
// 针对方法开启方法前和方法后的权限验证还有 角色认证
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private UserDetailsService userDetailsService;
   

    /**
     * 配置密码解析
     * @return
     */
    @Bean
    protected PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置用户名和密码
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭csrf防护 跨站请求防护
        http.csrf().disable()
                //表单登录
                .formLogin()
                //登录访问路径,与页面表单提交路径一致
                .loginProcessingUrl("/login")
                .and()
                //认证配置
                .authorizeRequests()
                .antMatchers("/login").permitAll()
                //任何请求
                .anyRequest()
                //都需要身份验证
                .authenticated();
    
        //配置退出
        http.logout()
                //退出路径
                .logoutUrl("/logout")
                ;
    }
}

导入以下依赖:

  <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

五张表

image-20230726093119359

登录验证的是t_user表

权限和角色分布在t_role和t_permission表中

实体类的创建(略 )自行完成

对应的mapper文件和映射文件的创建(略)

表数据请参考资料中的offcnpe.sql文件

业务逻辑层的编写如下

我们自定义一个业务逻辑层实现类

package com.tyhxzy.springsecurity.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.tyhxzy.springsecurity.entity.*;
import com.tyhxzy.springsecurity.mapper.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private TUserMapper userMapper;

    @Autowired
    private TUserRoleMapper tUserRoleMapper;

    @Autowired
    private TRoleMapper roleMapper;

    @Autowired
    private TRolePermissionMapper tRolePermissionMapper;


    @Autowired
    private TPermissionMapper tPermissionMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        LambdaQueryWrapper<TUser> userquery = new LambdaQueryWrapper<>();
        userquery.eq(TUser::getUsername,s);
        TUser user = userMapper.selectOne(userquery);

        // 存放权限的集合
        List<GrantedAuthority> ssp = null;
        if(user==null){
            throw  new UsernameNotFoundException("用户名不存在");
        }else{

            ssp = new ArrayList<>();
            LambdaQueryWrapper<TUserRole> q2 = new LambdaQueryWrapper<>();

            q2.eq(TUserRole::getUserId,user.getId());
            List<TUserRole> rids = tUserRoleMapper.selectList(q2);
            // 遍历中间表 把所有的角色id查询回来
            List<Integer> collect = rids.stream().map(sp -> sp.getRoleId()).collect(Collectors.toList());
            List<TRole> tRoles = roleMapper.selectBatchIds(collect);
            Stream<SimpleGrantedAuthority> simpleGrantedAuthorityStream = tRoles.stream().map(sp1 -> {
                return new SimpleGrantedAuthority(sp1.getKeyword());
            });
            List<SimpleGrantedAuthority> collect1 = simpleGrantedAuthorityStream.collect(Collectors.toCollection(ArrayList::new));
            ssp.addAll(collect1);

            // 查询权限
            // 也是先查询中间表
            for(TRole item:tRoles) {
                LambdaQueryWrapper<TRolePermission> q3 = new LambdaQueryWrapper<>();
                q3.eq(TRolePermission::getRoleId,item.getId());
                List<TRolePermission> tRolePermissions = tRolePermissionMapper.selectList(q3);
                List<Integer> pids = tRolePermissions.stream().map(sp1 -> sp1.getPermissionId()).collect(Collectors.toList());
                List<TPermission> tps = tPermissionMapper.selectBatchIds(pids);
                Stream<SimpleGrantedAuthority> rty = tps.stream().map(sp1 -> {
                    return new SimpleGrantedAuthority(sp1.getKeyword());
                });
                List<SimpleGrantedAuthority> collect2 = rty.collect(Collectors.toCollection(ArrayList::new));
                ssp.addAll(collect2);
            }



        }
        return new User(s,user.getPassword(),ssp);
    }
}

controller上编写

package com.tyhxzy.springsecurity.controller;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/list")
    @Secured(value = "ROLE_ADMIN")// 访问这个方法之前首先验证这个用户是否是ROLE_ADMIN这个角色
    public String hh(){
        return "哈哈";
    }

    @RequestMapping("/list1")
    @PreAuthorize("hasAuthority('CHECKITEM_QUERY')")  // 访问这个方法前首先验证这个用户是否有CHECKITEM_QUERY的权限
    public String hh1(){
        return "哈哈";
    }
}

在浏览器中访问http://localhost:8080/user/list 因为未登录会直接被spring security打到登录页面,

在登录页面输入 admin 密码是123456 这个人的角色时ROLE_ADMIN可以直接在页面输出哈哈

如果在登录页面输入 xiaoming 密码是123456 这个人的角色不是ROLE_ADMIN 所以会跳转到403页面

自定义登录失败和权限认证失败的内容返回给客户端,不要直接打印出来403页面或者登录失败

AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常

package com.tyhxzy.springsecurity.config;

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;

/**
 * 认证失败处理类 返回未授权
 * 用来解决认证过的用户访问无权限资源时的异常
 */
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
 
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setCharacterEncoding("utf-8");
        response.setContentType("application/json");

        response.getWriter().print("没有访问权限!");
    }
}


在WebSecurityConfig中添加如下代码:
 @Autowired
    private CustomAccessDeniedHandler deniedHandler;

  // 设置已经登录过 但是没有权限访问要走的对象
        http.exceptionHandling().accessDeniedHandler(deniedHandler);

AuthenticationFailureHandler 用来解决登录失败的异常

package com.tyhxzy.springsecurity.config;

import com.fasterxml.jackson.core.JsonProcessingException;
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;

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException, IOException {
        Map<String, Object> result = new HashMap<String, Object>();
        result.put("msg", "登录失败: "+exception.getMessage());
        result.put("status", 500);
        response.setContentType("application/json;charset=UTF-8");
        String s = new ObjectMapper().writeValueAsString(result);
        response.getWriter().println(s);
    }
}

在WebSecurityConfig中添加如下代码:
 @Autowired
   @Autowired
    private MyAuthenticationFailureHandler aa;

    http.csrf().disable()
                //表单登录
                .formLogin()
                //登录访问路径,与页面表单提交路径一致
                .loginProcessingUrl("/login").failureHandler(aa)

总结

常见的过滤器

image-20230727153431606

【1】WebAsyncManagerIntegrationFilter
将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。

【2】SecurityContextPersistenceFilter
在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

【3】HeaderWriterFilter
用于将头信息加入响应中。

【4】CsrfFilter
用于处理跨站请求伪造。

【5】LogoutFilter
用于处理退出登录。

【6】UsernamePasswordAuthenticationFilter
用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改

【7】DefaultLoginPageGeneratingFilter
如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

【8】BasicAuthenticationFilter
检测和处理 http basic 认证。

【9】RequestCacheAwareFilter
用来处理请求的缓存。

【10】SecurityContextHolderAwareRequestFilter
主要是包装请求对象request。

【11】AnonymousAuthenticationFilter
检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。

【12】SessionManagementFilter
管理 session 的过滤器。

【13】ExceptionTranslationFilter
处理 AccessDeniedException 和 AuthenticationException 异常。该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

【14】FilterSecurityInterceptor
可以看做过滤器链的出口。该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由ExceptionTranslationFilter过滤器进行捕获和处理。

【15】RememberMeAuthenticationFilter
当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

image-20230731093721767

常见的验证授权流程

一、Session-Cookie 机制 (web应用中最常见的)
当服务端需要对访问的客户端进行身份认证时,常用的做法是通过session-cookie 机制流程

image-20230726093141123

Session-Cookie 认证存在的问题:

当客户访问量增加,服务端需要存储大量的session会话,对服务端有很大考验
当服务端为集群时,用户登录其中一台服务器,会将session保存在该服务器的内存中,
但是当用户访问其他服务器时。会无法访问。(已经有了成熟的解决方案)可以采用使用缓存服务器来保证共享 第三方缓存来保存session由于依赖cookie,所以存在CSRF安全问题

前后端分离项目不共享session 演示问题

 //关闭csrf防护 跨站请求防护
        http.csrf().disable();
        http
                //认证配置
                .authorizeRequests()
                .antMatchers("/user/login","/user/add").permitAll();
        http.authorizeRequests().anyRequest().authenticated();
        http.cors();// 开启跨域




        // 设置登录失败的对象

        //配置退出
        http.logout()
                //退出路径
                .logoutUrl("/logout")
                ;

        //http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

二、Token 认证机制:

//http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

使用Jwt

跨域概念

跨域是如何引起的?
浏览器的安全策略  

 http://localhost:8080/index.html

http://localhost:8082/demo.html

协议:http
localhost:主机名
ip:127.0.0.1

端口号:8080

协议  ip  端口号 三者都一致 叫同源

浏览器默认只允许该项目只能访问 和自己协议 ip 端口号一致的项目中的资源
 http://localhost:8080/index.html  访问和自己 同源的demo.html是允许的

http://localhost:8080/index.html  访问   http://localhost:8082/demo.html  这就叫跨域

我们昨天是在 http://localhost:8080/index.html  去请求  http://localhost:8081/demo/a1  这默认浏览器是不允许的 这就叫跨域

Access to XMLHttpRequest at 'http://localhost:8081/demo/a1' from origin 'http://localhost:8080' has been blocked by CORS policy

origin 'http://localhost:8080'   从这个项目
请求  ttp://localhost:8081/demo/a1  另外一个项目
 has been blocked by CORS policy  被跨域政策拦截住

No 'Access-Control-Allow-Origin' header is present on the requested resource.
在请求头上我们没找见  Access-Control-Allow-Origin

@CrossOrigin:告知该controller中的所有请求 允许任何项目访问(不限制必须是同源)

@CrossOrigin(origins = "http://localhost:8080")  告知该controller请求只能由 http://localhost:8080这个源访问

如果你在controller类上写了@CrossOrigin
那么服务器会向客户端的响应头中写入Access-Control-Allow-Origin=“*”  不限制源

如果你在controller类上写了@CrossOrigin(origins = "http://localhost:8080")
那么服务器会向客户端响应头中写入Access-Control-Allow-Origin=“http://localhost:8080”

浏览器拿到这个Access-Control-Allow-Origin=“http://localhost:8080” 之后 会进行比对 看自己在不在人家的范围内 如果不在 还是被跨域拦截,如果在  就说明人家允许你进行访问

浏览器先根据同源策略对前端页面和后台交互地址做匹配,若同源,则直接发送数据请求:若不同源,则发送跨域请求°。

。当我们发起跨域请求时,如果是非简单请求,浏览器会帮我们自动触发预检请求,也就是OPTIONS请求,用于确认目标资源是否支持跨域。如果是简单请求,则不会触发预检,直接发出正常请求。

·服务器收到浏览器跨域请求后,根据自身配置返回对应文件头。若未配置过任何允许跨域,则文件头里不包含Access-Control-All ow-origin字段,若配置过域名,则返回 Ac cess-Control-Allow-origin+对应配置规则里的域名的方式。

●浏览器根据接收到的响应头里的Access-Con trol-Allow-origin字段做匹配,若无该字段,说明不允许跨域,从而抛出一个错误;若有该字段,则对字段内容和当前域名做比对,如果同源,则说明可以跨域,浏览器接受该响应;若不同源,则说明该域名不可跨域,浏览器不接受该响应,并抛出一个错误。

跨域问题

spring security 框架CsrfFilter权限高于@CrossOrigin

需要在配置类中关闭spring security的跨域保护

http.cors();

关闭表单登录

//http:formLogin.failureHandler();

配置放行界面

http.authorizeRequests().antMatchers("/demo/a1").permitAll();
http.csrf().disable();//关闭跨站请求伪造

告诉spring security不需要再把认证过的数据往session存放

http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

自定义过滤器

表单登录失败

class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler
package com.mmkj.offcnpe.config;

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;

/**
 * 适用于表单登录失败
 */
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>();
        stringObjectHashMap.put("message","登录失败");
        stringObjectHashMap.put("code",2002);
        //httpServletResponse.sendRedirect("/offcnpe/checkgroup/findAllCheckItems");
        httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap));
    }
}

已登陆权限不足跳转

package com.mmkj.offcnpe.config;

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.util.HashMap;
//已登陆权限不足跳转
@Component
public class MyCustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>();
        stringObjectHashMap.put("message","权限不足");
        stringObjectHashMap.put("code",2001);
        httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap));
    }
}

非表单登陆失败

package com.mmkj.offcnpe.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
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;
//非表单登陆失败
@Component
public class MySecurity403 implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setCharacterEncoding("utf-8");
        httpServletResponse.setContentType("application/json");
        HashMap<String, Object> stringObjectHashMap = new HashMap<String, Object>();
        stringObjectHashMap.put("message","权限不足");
        stringObjectHashMap.put("code",2003);
        httpServletResponse.getWriter().print( new ObjectMapper().writeValueAsString(stringObjectHashMap));
    }
}

表单登录成功处理器

class ForwardAuthenticationSuccessHandler implements AtuhenticationSuccessHandler

token验证登录状态

package com.mmkj.offcnpe.config;
 
import io.jsonwebtoken.Jwts;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.stream.Collectors;


//spring的过滤器,每次请求都进行拦截
@Component
public class OncePerOverRequestFilter extends OncePerRequestFilter {
    @Autowired
    private RedisTemplate<String, Object> template;

    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String auth = httpServletRequest.getHeader("auth");//获取请求头中加密信息
        if (auth != null && auth.equals("")) {
            auth = auth.substring("Bearer".length());
            Object nameObj = Jwts.parser().setSigningKey("yxh111").parseClaimsJws(auth).getBody().get("username");//解密获取用户名称
            if (nameObj != null) {
                LinkedHashMap<String, Object> redisDetail_Data = (LinkedHashMap<String, Object>) template.opsForValue().get(nameObj.toString());//authorties对象集合,格式不对
                ArrayList<LinkedHashMap<String, String>> a2 = (ArrayList<LinkedHashMap<String, String>>) redisDetail_Data.get("authorities");
                //List<GrantedAuthority> authority = AuthorityUtils.commaSeparatedStringToAuthorityList(String.join(",", a2.stream().map(v -> v.get("authority")).collect(Collectors.toList())));
                List<SimpleGrantedAuthority> collect = a2.stream().map(v -> v.get("authority")).map(v1 -> new SimpleGrantedAuthority(v1)).collect(Collectors.toList());
                UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(redisDetail_Data.get("username").toString(), redisDetail_Data.get("password").toString(), collect);//创建UsernamePasswordAuthenticationToken对象
                SecurityContextHolder.getContext().setAuthentication(upat);//放入上下文
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);//放行执行链
    }
}

加载过滤器

package com.mmkj.offcnpe.config;

import org.springframework.beans.factory.annotation.Autowired;
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.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@EnableWebSecurity//开启安全管理配置
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private MyCustomAccessDeniedHandler myCustomAccessDeniedHandler;
    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Autowired
    private OncePerOverRequestFilter oncePerOverRequestFilter;

    @Autowired
    private MySecurity403 mySecurity403;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override//自定义认证
    protected void configure(HttpSecurity http) throws Exception {
        //http.formLogin().failureHandler(myAuthenticationFailureHandler);//表单登录失败跳转
        http.authorizeRequests().antMatchers("/offcnpe/user/login").permitAll().anyRequest().authenticated();//放行请求
        http.exceptionHandling().accessDeniedHandler(myCustomAccessDeniedHandler);//已登陆权限不足
        http.exceptionHandling().authenticationEntryPoint(mySecurity403);//未登录权限不足
        http.csrf().disable();//关闭跨站请求伪造
        http.cors ();//关闭跨域保护
        http.addFilterAt(oncePerOverRequestFilter, UsernamePasswordAuthenticationFilter.class);//登录成功请求头中状态验证
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//告诉spring security不需要再把认证过的数据往session存放
    }
}

controller中登录判定

package com.mmkj.offcnpe.controller;


import com.mmkj.offcnpe.entity.TUser;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author yxh
 * @since 2023-07-17
 */
@RestController
@RequestMapping("/offcnpe/user")
@CrossOrigin
public class UserController {
    @Qualifier("userdetails")
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private RedisTemplate<String, Object> template;

    @RequestMapping("/login")
    public String login(@RequestBody TUser user) {
        if (!(SecurityContextHolder.getContext().getAuthentication() instanceof UsernamePasswordAuthenticationToken)){//判断当前上下文是否为空
            BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
            UserDetails userDetails = userDetailsService.loadUserByUsername(user.getUsername());
            if (bCryptPasswordEncoder.matches(user.getPassword(), userDetails.getPassword())) {
                return "密码错误";
            } else {
                template.opsForValue().set(user.getUsername(), userDetails);
                UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
                SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                JwtBuilder builder = Jwts.builder();
                Calendar calendar = Calendar.getInstance();
                calendar.add(Calendar.HOUR,1);
                builder.setId("idxxx");
                builder.setIssuer("张三");
                builder.setExpiration(calendar.getTime());
                builder.setIssuedAt(new Date());
                builder.signWith(SignatureAlgorithm.HS256,"yxh111");
                HashMap<String, Object> stringObjectHashMap = new HashMap<>();
                stringObjectHashMap.put("username",userDetails.getUsername());
                builder.addClaims(stringObjectHashMap);
                return builder.compact();
            }
        }
        return "登录成功";
    }

    @RequestMapping("/admin")
    @Secured("ROLE_ADMIN")
    public String admin() {
        return "admin";
    }

    @RequestMapping("/publicList")
    public String publicList() {
        return "user";
    }

    @RequestMapping("/health")
    @Secured("ROLE_HEALTH_MANAGER")
    public String user() {
        return "health-manager";
    }

    @RequestMapping("/delUser")
    @PreAuthorize("hasAuthority('USER_DELETE')")
    public String delUser() {
        return "delUser";
    }
}

posted @ 2023-07-26 21:05  YxinHaaa  阅读(11)  评论(0编辑  收藏  举报