spring-security2.6.3+JWT认证授权

文章目录

    • 一. 前言
    • 二. 基本原理
      • 2.1 加载过程
    • 三. 存储加密-BCryptPasswordEncoder
    • 四.spring-security + JWT 前后端分离认证
      • 4.1 jwt工具类`用来加密解密token`
      • 4.2 SecurityConfig配置
      • 4.3 实现加载用户指定数据的核心接口
      • 4.4 登录认证流程
      • 4.5 自定义登录接口
    • 五.spring-security授权
      • 5.1 注解权限控制
      • 5.2 自定义权限校验注解
    • 六.处理器
      • 6.1 自定义处理器
      • 6.2认证失败异常处理器
      • 6.3 授权失败异常处理器
      • 6.4 注入SercurityConfig的Configure方法
    • 七.跨域
    • 八. CSRF
    • 九.Oauth2.0
      • 9.1github注册密钥授权
    • 最后


一. 前言

比shiro更适合与spring体系相结合.主要永用户认证用户授权
shiro使用请看此处shiro认证授权加密验证的脚手架搭建

相关引用: b站编程不良人,三更草堂,尚硅谷杨博超

spring security认证相关文章视频

spring security授权相关文章视频

自己总结的项目demo-gitee

自己总结的项目demo-github


二. 基本原理

SpringSecurity 本质是一个过滤器链.
spring-security2.6.3+JWT认证授权_第1张图片

FilterSecurityInterceptor 是一个方法级的权限过滤器,基本位于过滤链的最底部
ExceptionTranslationFilter 是个异常过滤器,用来处理在认证授权过程中地出的异常
UsernamePasswordAuthenticationFilter 对/login的POST请求做拦截,校验表单中用户名,密码。

spring-security2.6.3+JWT认证授权_第2张图片
spring-security2.6.3+JWT认证授权_第3张图片

2.1 加载过程

一般来说,常见的安全管理技术栈的组合是这样的
SSM + Shiro.
Spring Boot/Spring Cloud + Spring Security.(springboot自动装配)
spring-security2.6.3+JWT认证授权_第4张图片

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
  • AuthenticationManager接口:定义了认证Authentication的方法
  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

spring-security2.6.3+JWT认证授权_第5张图片

不使用boot一般需要如下加载
spring-security2.6.3+JWT认证授权_第6张图片
spring-security2.6.3+JWT认证授权_第7张图片


三. 存储加密-BCryptPasswordEncoder

  • $2a是BCrypt的版本号10是“强度"(BCrypt中的日志轮数),接着22位是随机生成的.即密文的前29位是生成的.包含了salt信息.之后的31位是生成的密文
  • 同样的密码可以生成不同的 密文而且还可以通过 matches 方法进行匹配验证。因为密文中包括 salt 和 使用 salt 加密后的 hash。因为每次的 salt 不同,因此每次的 hash 也不同。这样就可以使得相同的 明文 生成不同的 密文,而密文中包含 salt 和 hash,因此验证过程和生成过程也是相同的。
  • 可以在用户注册时将加密结果存储

密码加密与校验举例
spring-security2.6.3+JWT认证授权_第8张图片

四.spring-security + JWT 前后端分离认证

认证
UserDetailsService当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。所以我们要通过自定义逻辑控制认证逻辑。

如果需要自定义逻辑时,只需要实现UserDetailsService接口即可

两个重要的接口

  • UserDetailsService接口:查询数据库用户名和密码过程
    创建类继承JsernamePasswordAuthenticationFilter.重写三个方法
    创建类实现UserDetailService,编写查询数据过程,返回User对象,这个User对象是安全框架提供对象
  • PasswordEncoder接口: 数据加密接口,用于返回User对象里面密码加密

授权
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。

  • 用户对应一个角色
  • 一个角色对应一组权限
    spring-security2.6.3+JWT认证授权_第9张图片

引入核心pom

       <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        <mybatis.plus>3.5.1mybatis.plus>
        <freemarker>2.3.31freemarker>
        <jjwt>0.9.1jjwt>
        <redisson>3.17.6redisson>
        <commons-lang3>3.12.0commons-lang3>
        <kaptcha>0.0.9kaptcha>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>${jjwt}version>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>${mybatis.plus}version>
        dependency>
        
        <dependency>
            <groupId>org.freemarkergroupId>
            <artifactId>freemarkerartifactId>
            <version>${freemarker}version>
        dependency>
        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redisson-spring-boot-starterartifactId>
            <version>${redisson}version>
        dependency>

        
        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>${commons-lang3}version>
        dependency>
        
        <dependency>
            <groupId>com.github.axetgroupId>
            <artifactId>kaptchaartifactId>
            <version>${kaptcha}version>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-validationartifactId>
        dependency>
                <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
    dependencies>

    dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-dependenciesartifactId>
                <version>2.6.3version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

4.1 jwt工具类用来加密解密token

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 36 * 60 * 1000L;// 36 * 60 *1000  36分钟
    //设置秘钥明文
    public static final String JWT_KEY = "admin";
    // JWT存储的请求头
    public static final String TOKEN_AUTHORIZATION = "Authorization";
    // JWT 负载中拿到开头
    public static final String TOKEN_BEARER = "Bearer";


    public static String getUUID() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }

    /**
     * 加密后的秘钥 secretKey
     *
     * @return
     */
    public static SecretKey generateKey() {
        byte[] encodedKey = Base64.getEncoder().encode(JwtUtil.JWT_KEY.getBytes(StandardCharsets.UTF_8));
        return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (ttlMillis == null) {
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("yuanjie")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(SignatureAlgorithm.HS256, generateKey()) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 生成jwt
     *
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jwt
     *
     * @param subject   token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 创建token
     *
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    /**
     * 解析获取荷载
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        return Jwts.parser()
                .setSigningKey(generateKey())
                .parseClaimsJws(jwt)
                .getBody();
    }

    /**
     * 从token获取用户名
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static String getUserNameFromToken(String jwt) throws Exception {
        return parseJWT(jwt).getSubject();
    }

    /**
     * 验证token是否有效
     *
     * @param
     * @throws Exception
     */
    public static boolean validateToken(String token, UserDetails userDetails) throws Exception {
        String username = getUserNameFromToken(token);
        return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
    }

    /**
     * 判断token是否失效
     *
     * @param
     * @throws Exception
     */
    public static boolean isTokenExpired(String token) throws Exception {
        Date expired = getExpiredDateFromToken(token);
        return expired.before(new Date());
    }

    /**
     * 从token中获取时间
     *
     * @param
     * @throws Exception
     */
    public static Date getExpiredDateFromToken(String token) throws Exception {
        Claims claims = parseJWT(token);
        return claims.getExpiration();
    }

    /**
     * 验证token是否可以被刷新
     *
     * @param
     * @throws Exception
     */
    public boolean canRefresh(String token) throws Exception {
        return !isTokenExpired(token);
    }

    /**
     * 刷新token
     *
     * @param token
     * @return
     */
    public String refreshToken(String token) throws Exception {
        return createJWT(getUserNameFromToken(token), JWT_TTL);
    }

    public static void main(String[] args) throws Exception {
        String jwt = JwtUtil.createJWT("123");
        System.out.println("加密后" + jwt);
        Claims claims = JwtUtil.parseJWT(jwt);
        String subject = claims.getSubject();
        System.out.println("解密后" + subject);
        System.out.println("有效时间:" + claims.getExpiration());
    }
}

4.2 SecurityConfig配置

/**
 * @author WangJiaHui
 * @description: security配置类
 * @ClassName SercurityConfig
 * @date 2022/9/7 20:30
 */
@Configuration
public class SercurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private DataSource dataSource;
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Resource
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
    @Resource
    private RestfulAccessDeniedHandler restfulAccessDeniedHandler;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    @Bean
    // 注入BCryptPasswordEncoder编码器
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    // 认证管理器
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 放行路径,不走过滤器链
        web.ignoring().antMatchers(
                "css/**",
                "js/**",
                "/index.html",
                "favicon.ico",
                "/captcha");
    }
    // 重写configure(HttpSecurity http) 自定义登录页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/login","/logout")
                .permitAll() // 放行的url
                .anyRequest().authenticated() // 除上述放行的url,其余全部鉴权认证
                .and()
                .rememberMe().tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60 * 36) // 设置token有效期36min
                .userDetailsService(userDetailsService)
                .and()
                // 关闭csrf
                .csrf().disable()
                // 基于token,不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .headers()
                .cacheControl();// 缓存关闭
        // 添加jwt 登录授权过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加自定义未授权和未登录结果返回
        http.exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint) // 认证失败异常处理器
                .accessDeniedHandler(restfulAccessDeniedHandler); // 授权失败异常处理器
    }
}


4.3 实现加载用户指定数据的核心接口

/**
 * 自定义实现UserDetail认证
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Resource
    AdminMapper adminMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<Admin> wrapper =
                new QueryWrapper<Admin>()
                        .eq("username", username);
        Admin admin = adminMapper.selectOne(wrapper);
        if(!Optional.ofNullable(admin).isPresent()){
            throw new UsernameNotFoundException("用户不存在!");
        }
        // 鉴权凭证
        List<GrantedAuthority> role =
                AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale,ROLE_admin"); // 基于角色访问需要前缀ROLE_

        return new LoginEntity(admin,null);
    }
}

4.4 登录认证流程

大致思路:
第一部分

    1. 在service层 Authentication.Manager authenticate进行用户认证
    1. 如果认证没通过,给出对应的提示
    1. 如果认证通过了,使用userid生成一个加密jwt jwt–>token存入R返回
    1. 把完整的用户对象存入redis 用 token:userid 作为key

第二部分

    1. 创建认证过滤器继承OncePerRequestFilter.在securityConfig配置过滤器顺序在最前
    1. 获取token
    1. 解析token
    1. 用解析的userId token:userId作为key寻找redis中的信息比对.
    1. 存入SecurityContextHolder,让后续的过滤器链获取信息
      spring-security2.6.3+JWT认证授权_第10张图片

4.5 自定义登录接口

步骤一
spring-security2.6.3+JWT认证授权_第11张图片
controller接口

@RestController
public class LoginController {
    @Resource
    private IAdminService adminService;
    /**
     *  登录之后返回token
      */
    @PostMapping("/login")
    public RespVO login(@Valid @RequestBody AdminLoginVO adminLoginVO,
                        HttpServletRequest request){
         return adminService.login(adminLoginVO,request);
    }
    /**
     * 获取当前登录用户信息
     */
    @GetMapping("/admin/info")
    public RespVO getAdminInfo(Principal principal){
        if(!Optional.ofNullable(principal).isPresent()){
            return null;
        }
        String username = principal.getName();
        return adminService.getAdminByUserName(username);
    }

    /**
     * 退出登录
     */
    @PostMapping("/logout")
    public RespVO logout(){
        return adminService.logout();
    }
}

认证授权service处理

spring-security2.6.3+JWT认证授权_第12张图片
AuthenticationManager 是认证管理器,在 Spring Security 中有全局AuthenticationManager,也可以有局部AuthenticationManager。全局的AuthenticationManager用来对全局认证进行处理,局部的AuthenticationManager用来对某些特殊资源认证处理。当然无论是全局认证管理器还是局部认证管理器都是由 ProviderManger 进行实现。 每一个ProviderManger中都代理一个AuthenticationProvider的列表,列表中每一个实现代表一种身份认证方式。认证时底层数据源需要调用 UserDetailService 来实现。

    1. 默认自动配置创建全局AuthenticationManager 默认找当前项目中是否存在自定义 UserDetailService 实例自动将当前项目 UserDetailService 实例设置为数据源
    1. 默认自动配置创建全局AuthenticationManager 在工厂中使用时直接在代码中注入即可自定义全局 AuthenticationManager
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements IAdminService {
    @Resource
    UserDetailsService userDetailsService;
    @Resource
    PasswordEncoder passwordEncoder;
    @Resource
    private AdminMapper adminMapper;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private AuthenticationManager authenticationManager;
    @Override
    public RespVO login(AdminLoginVO adminLoginVO, HttpServletRequest request) {
        if(!adminLoginVO.getCode().equals(request.getSession().getAttribute("captcha"))){
            return RespVO.error("验证码输入错误!");
        }
        // 登录 authenticationManager最终会调用UserDetailsService实现方法
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(adminLoginVO.getUsername(), adminLoginVO.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // UserDetailsService返回对象
        LoginEntity loginEntity = (LoginEntity) authenticate.getPrincipal();
        /**
         * userDetails会在底层调用username和password字段进行匹配认证.可以在
         * AuthenticationEntryPoint中执行认证失败处理
         */
//        if(!passwordEncoder
//                .matches(adminLoginVO.getPassword(), loginEntity.getAdmin().getPassword())){
//            return RespVO.error("用户名或密码不正确");
//        }
        if(!loginEntity.isEnabled()){
            return RespVO.error("账号被禁用,请联系管理员!");
        }
        // 生成token 即加密username
        String jwt = JwtUtil.createJWT(adminLoginVO.getUsername());
        Map<String, String> map = new HashMap<>();
        map.put("tokenHead",JwtUtil.TOKEN_BEARER);
        map.put("token",jwt);

        // 键token:username   值loginEntity对象
        RBucket<Object> token = redissonClient.getBucket("token:"+loginEntity.getUsername());
        token.set(loginEntity,36, TimeUnit.MINUTES);
        return RespVO.ok("登录成功!").put(map);
    }

    @Override
    public RespVO logout() {
        // 获取SecurityContextHolder中的用户id
        // UserDetailsService返回对象
        LoginEntity loginEntity = (LoginEntity) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String username = loginEntity.getUsername();
        redissonClient.getBucket("token:" + username).delete();
        return RespVO.ok("退出成功!");
    }

    @Override
    // TODO 查库吗?
    public RespVO getAdminByUserName(String username) {
        QueryWrapper<Admin> wrapper = new QueryWrapper<Admin>()
                .eq("username", username);
        Admin admin = adminMapper.selectOne(wrapper);
        if(!Optional.ofNullable(admin).isPresent()){
            return RespVO.error("用户信息不存在!");
        }
        if(!admin.getEnabled()){
            return RespVO.error("用户已被禁用,请联系管理员!");
        }
        return RespVO.ok().put(admin);
    }

}

步骤二.回传token
spring-security2.6.3+JWT认证授权_第13张图片
jwt token回传解密验证数据库信息.

/**
 * @ClassName JwtAuthenticationTokenFilter
 * @Description jwt token回传解密验证数据库信息.
 * @Author YuanJie
 * @Date 2022/8/25 20:43
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Resource
    private RedissonClient redissonClient;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String authHeader = request.getHeader(JwtUtil.TOKEN_AUTHORIZATION);
        // token不存在
        if (StringUtils.isBlank(authHeader) || !authHeader.startsWith(JwtUtil.TOKEN_BEARER)) {
            filterChain.doFilter(request, response);
            return;
        }
        // TOKEN_HEADER=TOKEN_HEAD TOKEN  截取token
        String authToken = authHeader.substring(JwtUtil.TOKEN_BEARER.length());
        // 解析token中的username
        String subject;
        try {
            subject = JwtUtil.getUserNameFromToken(authToken);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        // 从redis中获取用户信息 对应登录加密存储redis的键
        String redisKey = "token:" + subject;
        // redis的redisson整合框架
        RBucket<LoginEntity> bucket = redissonClient.getBucket(redisKey);
        LoginEntity loginEntity = bucket.get();
        if (!Optional.ofNullable(loginEntity).isPresent()) {
            throw new RuntimeException("验证已过期,请重新登录!");
        }
        // 存入SecurityContextHolder,让后续的过滤器链获取信息 三参构造标记已认证
        // TODO 权限信息
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginEntity, null, null);
        // 放入spring scurity缓存
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

注入Security的configure方法

        // 配置过滤器顺序
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

五.spring-security授权

FilterSecurityInterceptor 是一个方法级的权限过滤器,基本位于过滤链的最底部
FilterInvocationSecurityMetadataSource 获取某个受保护的安全对象object的所需要的权限信息,是一组ConfigAttribute对象的集合.该Filter中的getAttributes方法获取到的是访问每条路径所需要的角色,并将其存储在类型为ConfigAttribute的Collection中作为返回值;接下来就要判断当前用户角色是否与访问路径所需角色相匹配,实现AccessDecisionManager接口的MyAccessDecisionManager类就是完成这件事的。
AccessDecisionManagerHandler 访问决策管理器

启动类或spring-security配置类 标记@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true) 分别对应三个注解启动

5.1 注解权限控制

// 角色认证  匹配的字符串需要添加前缀“ROLE_“  不支持Spring 表达式语言
@GetMapping("/hello1")
@Secured({"ROLE_normal","ROLE_admin"})
public String hello1() {
    return "hello1";
}

// 基于权限认证 拥有normal或者admin角色的用户都可以方法helloUser()方法 支持Spring 表达式语言
// 自动拼接ROLE_.要求数据库对应角色也具有ROLE_前缀
@GetMapping("/hello2")
@PreAuthorize("hasAnyRole('normal','admin')")
public String hello2() {
    return "hello2";
}

//@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限
@GetMapping("/hello3")
@PostAuthorize(" returnObject!=null &&  returnObject.username == authentication.name")
public User hello3() {
        Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        User user;
        if("anonymousUser".equals(pricipal)) {
            user = null;
        }else {
            user = (User) pricipal;
        }
        return user;
}

5.2 自定义权限校验注解

@Component("exp")
public class SGExpressionRoot {

    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 这里的对象已经经过security过滤器链.认证过滤器已在SecurityContextHolder存储用户信息
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}
    @RequestMapping("/hello")
    @PreAuthorize("@exp.hasAuthority('system:dept:list')")
    public String hello(){
        return "hello";
    }

六.处理器

6.1 自定义处理器

  • 在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在
    ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
  • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
  • 如果是授权过程中出现的异常会被封装成AccessDeniedException:然后调用*AccessDeniedHandler**对象的方法去进行异常处理。
  • 所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置SpringSecurity即可。

自定义处理器可实现以下接口,并注入容器在SecurityConfig中配置.

  • AuthenticationEntryPoint 认证失败异常处接口

  • AccessDeniedHandler 授权失败异常处理接口

  • AuthenticationSuccessHandler 认证成功处接口

  • AuthenticationFailureHandler 认证失败处接口

  • LogoutSuccessHandler 登出成功接口

6.2认证失败异常处理器

@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        RespVO respVO = RespVO.error(401,"用户名或密码不正确!");
        writer.write(new ObjectMapper().writeValueAsString(respVO));
        writer.flush();
        writer.close();
    }
}

6.3 授权失败异常处理器

@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        RespVO respVO = RespVO.error(403,"权限不足请联系管理员");
        writer.write(new ObjectMapper().writeValueAsString(respVO));
        writer.flush();
        writer.close();
    }
}

6.4 注入SercurityConfig的Configure方法


		http.formLogin().successHandler(successHandler) // 配置成功处理器
                .failureHandler(failureHandler); // 配置认证失败处理器
        //配置登出成功处理器
        http.logout()
				.logoutSuccessHandler(logoutSuccessHandler);
        // 配置异常处理器
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint) // 认证失败异常处理器
                .accessDeniedHandler(accessDeniedHandler); // 授权失败异常处理器

七.跨域

①先对SpringBooti配置,运行跨域请求

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .maxAge(3600);
    }
}

Security.Config.java

        // 允许跨域
        http.cors();

八. CSRF

跨站请求伪造(英语:Cross-.site request forgery),也被称为one-click attack或者session
riding,通常缩写为CSRF或者XSRF,是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了wb中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从Spring Security4.0开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用程序,Spring Security CSRF会针对PATCH,POST,PUT和DELETE方法进行防护。跨域无法请求.

我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

九.Oauth2.0

声明: 还未研究透彻
OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。

OAuth2.0:【一种协议标准,现在都使用OAuth2.0】实际上就是服务器开放了一些查询用户信息的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权,授权成功会拿到token,根据token调用OpenAPI获取用户信息

spring-security2.6.3+JWT认证授权_第14张图片
spring-security2.6.3+JWT认证授权_第15张图片
在这里插入图片描述
从上图中我们可以看出六个步骤之中,B是关键,即用户怎样才能给于客户端授权。同时会发现0AUth2中包含四种不同的角色:

  • Client:第三方应用。
  • Resource Owner:资源所有者。
  • Authorization Server:授权服务器。
  • Resource Server:资源服务器。

授权码模式:
spring-security2.6.3+JWT认证授权_第16张图片
spring-security2.6.3+JWT认证授权_第17张图片

spring-security2.6.3+JWT认证授权_第18张图片

9.1github注册密钥授权

该错误详解StackoverflowError Spring Security Oauth clientDetailsService

注意
spring-security2.6.3+JWT认证授权_第19张图片

SecurityConfig

    // 重写configure(HttpSecurity http) 自定义登录页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 前后端分离 不可开启csrf防护,不通过session
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login.html","/login/**").permitAll()
                .antMatchers("/test/**").permitAll()
//                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        // 允许跨域
        http.cors();
        // 开启oauth2请求
        http.oauth2Login();
    }

spring-security2.6.3+JWT认证授权_第20张图片
在这里插入图片描述

  1. 后端在 .yml 配置中做好相关配置
    github.* 为自定义,无提示.
spring:
  application:
    name: springSecurityTest
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: xxxxx
            client-secret: xxxxxxxxxxxxx
            #一定要与重定向回调URL一致
            redirect-uri: http://localhost:8080/login/oauth2/code/github
server:
  port: 8080

spring-security2.6.3+JWT认证授权_第21张图片

spring-security2.6.3+JWT认证授权_第22张图片

spring-security2.6.3+JWT认证授权_第23张图片

最后

还可以可以用spring-security做自己的认证服务器和授权服务器.但是管理比较混乱.博主水平不足

小坑

  1. public void configure(WebSecurity web)放行的路径不走过滤器链.
  2. protected void configure(HttpSecurity http) 会走过滤器链.
  3. 建议静态公开路径放在public void configure(WebSecurity web)中放行
    原因: 倘若配置了授权过滤器,其不允许匿名登陆. 授权一般使用security的授权实现接口过滤器.
    而认证一般是通过实现UserDetailsService方法. 这会导致登录时,先走过滤器判断权限.而此时你没有和数据库比对认证,同时也就不会给予认证角色权限.这就导致没有权限信息就先进入授权过滤器.因此必为匿名登录.造成失败!
  4. 注意过滤器配置顺序!
  5. 复杂类型不能反序列化
    spring-security2.6.3+JWT认证授权_第24张图片
    忽略了该字段成功.
    spring-security2.6.3+JWT认证授权_第25张图片
    spring-security2.6.3+JWT认证授权_第26张图片

你可能感兴趣的:(Java体系,spring,java,spring,boot)