Spring Security + jwt入门

Spring Security + jwt入门

    • 1.SpringSecurity 框架简介
    • 2.认证
    • 3.SpringSecurity 入门案例
    • 4. 授权
    • 5.自定义失败处理
    • 6.跨域
    • 7.小知识

1.SpringSecurity 框架简介

概要

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的 成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方 案。

正如你可能知道的关于安全方面的两个主要区域是”认证”和“授权”,Web 应用的安全性包括用户认证(Authentication)用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

  1. 用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问 该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。说就是系统认为用户是否能登录
  2. 用户授权:验证某个用户是否有权限执行某个操作。在一个系统中,不同用户 所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以 进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的 权限。就是系统判断用户是否有权限去做某些事情。

历史

Spring Security 开始于 2003 年年底,spring 的 acegi 安全系统。 起因是 Spring 开发者邮件列表中的一个问题,有人提问是否考虑提供一个基于 spring 的安全实现。

Spring Security 以 The Acegi Secutity System for Spring 的名字始于 2013 年晚些 时候。一个问题提交到 Spring 开发者的邮件列表,询问是否已经有考虑一个机遇 Spring 的安全性社区实现。那时候 Spring 的社区相对较小(相对现在)。实际上 Spring 自己在 2013 年只是一个存在于 ScourseForge 的项目,这个问题的回答是一个值得研究的领 域,虽然目前时间的缺乏组织了我们对它的探索。

考虑到这一点,一个简单的安全实现建成但是并没有发布。几周后,Spring 社区的其他成员询问了安全性,这次这个代码被发送给他们。其他几个请求也跟随而来。到 2014 年一月大约有 20 万人使用了这个代码。这些创业者的人提出一个 SourceForge 项目加入是为了,这是在 2004 三月正式成立。

在早些时候,这个项目没有任何自己的验证模块,身份验证过程依赖于容器管理的安全性和 Acegi 安全性。而不是专注于授权。开始的时候这很适合,但是越来越多的用户请求额外的容器支持。容器特定的认证领域接口的基本限制变得清晰。还有一个相关的问题增加新的容器的路径,这是最终用户的困惑和错误配置的常见问题。

Acegi 安全特定的认证服务介绍。大约一年后,Acegi 安全正式成为了 Spring 框架的子项目。1.0.0 最终版本是出版于 2006 -在超过两年半的大量生产的软件项目和数以百计的改进和积极利用社区的贡献。

Acegi 安全 2007 年底正式成为了 Spring 组合项目,更名为"Spring Security"。

对比

  1. Spring Security

    Spring 技术栈的组成部分。

    Spring Security + jwt入门_第1张图片

    Spring Security通过提供完整可扩展的认证和授权支持保护你的应用程序。

    特点:

    • 和 Spring 无缝整合。
    • 全面的权限控制。
    • 专门为 Web 开发而设计。
      • 旧版本不能脱离 Web 环境使用。
      • 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独引入核心模块就可以脱离 Web 环境。
    • 重量级。
  2. Shiro

    Spring Security + jwt入门_第2张图片

    特点

    • 轻量级。Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求的互联网应用有更好表现。
    • 通用性。
      • 好处:不局限于 Web 环境,可以脱离 Web 环境使用。
      • 缺陷:在 Web 环境下一些特定的需求需要手动编写代码定制。
  3. 安全管理技术栈的组合

    • SSM + Shiro
    • Spring Boot/Spring Cloud + Spring Security

2.认证

登陆校验流程

Spring Security + jwt入门_第3张图片

SpringSecurity 本质是一个过滤器链:

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
org.springframework.security.web.context.SecurityContextPersistenceFilter 
org.springframework.security.web.header.HeaderWriterFilter
org.springframework.security.web.csrf.CsrfFilter
org.springframework.security.web.authentication.logout.LogoutFilter 
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter 
org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter 
org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
org.springframework.security.web.authentication.AnonymousAuthenticationFilter 
org.springframework.security.web.session.SessionManagementFilter 
org.springframework.security.web.access.ExceptionTranslationFilter 
org.springframework.security.web.access.intercept.FilterSecurityInterceptor

Spring Security + jwt入门_第4张图片

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

**ExceptionTranslationFilter:**处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

**FilterSecurityInterceptor:**负责权限校验的过滤器。

认证流程详解

Spring Security + jwt入门_第5张图片

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

3.SpringSecurity 入门案例

  1. 准备sql

    CREATE TABLE `test`.`user`  (
      `id` bigint NOT NULL COMMENT '用户id',
      `username` varchar(255) NOT NULL COMMENT '用户名',
      `password` varchar(255) NOT NULL COMMENT '用户密码',
      PRIMARY KEY (`id`, `username`, `password`)
    );
       
    insert into users values(1,'admin','$2a$10$80Hk/qT.heiw.q/4MCzjpOvUOxbw.QvLXoPb/g4fdI1koDC7iE2S6');
    insert into users values(2,'user','$2a$10$FrzW1aL0t.Dym6mvMp/1sOjzNynbgN6wQoABR17cV1lJKK//js7cy');
    
  2. 创建spring-security-study项目

  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>
    
        <groupId>pers.itqiqigroupId>
        <artifactId>spring-security-studyartifactId>
        <version>1.0-SNAPSHOTversion>
    
        <properties>
            <maven.compiler.source>8maven.compiler.source>
            <maven.compiler.target>8maven.compiler.target>
        properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-webartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-testartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-securityartifactId>
            dependency>
            <dependency>
                <groupId>org.projectlombokgroupId>
                <artifactId>lombokartifactId>
            dependency>
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-starter-data-redisartifactId>
            dependency>
            <dependency>
                <groupId>com.baomidougroupId>
                <artifactId>mybatis-plus-boot-starterartifactId>
                <version>3.5.2version>
            dependency>
            <dependency>
                <groupId>cn.hutoolgroupId>
                <artifactId>hutool-jwtartifactId>
            dependency>
        dependencies>
    
        <dependencyManagement>
            <dependencies>
                <dependency>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-dependenciesartifactId>
                    <version>2.7.3version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
                <dependency>
                    <groupId>cn.hutoolgroupId>
                    <artifactId>hutool-bomartifactId>
                    <version>5.8.5version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.bootgroupId>
                    <artifactId>spring-boot-maven-pluginartifactId>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackagegoal>
                            goals>
                        execution>
                    executions>
                plugin>
            plugins>
        build>
        
    project>
    
  4. 主启动类

    @SpringBootApplication
    public class SpringSecurityMain {
        public static void main(String[] args) {
            SpringApplication.run(SpringSecurityMain.class, args);
        }
    }
    
  5. application.yml配置文件

    server:
      port: 19266
    spring:
      application:
        name: spring-security-study
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
        username: root
        password: 123456
      redis:
        url: redis://localhost:6379
    
  6. 实体类

    @Data
    @TableName("user")
    public class Users implements Serializable {
        @TableId
        private Long id;
        @TableField
        private String username;
        @TableField
        private String password;
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class RequestResult {
    
        // 状态码
        private Integer code;
        // 状态信息
        private String msg;
        // 获取的数据
        private Object data;
        
        public RequestResult(Integer code, String msg) {
            this.code = code;
            this.msg = msg;
        }
    
    }
    
    public class LoginUser extends User {
    
        private Users users;
    
        public LoginUser(Users users, Collection<? extends GrantedAuthority> authorities) {
            super(users.getUsername(), users.getPassword(), authorities);
            setUsers(users);
        }
    
        public LoginUser(Users users, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
            super(users.getUsername(), users.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
            setUsers(users);
        }
    
        public Users getUsers() {
            return users;
        }
    
        public void setUsers(Users users) {
            this.users = users;
        }
    }
    
  7. BeanFactory注入管理

    @Component
    public class BeanFactory {
    
        @Resource
        private RedisTemplate<Object, Object> redisTemplate;
    
        @Bean
        public BCryptPasswordEncoder bCryptPasswordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean RedisTemplate newRedisTemplate() {
            return redisTemplate;
        }
    }
    
  8. service接口层

    public interface LoginService {
        RequestResult login(Users user);
    }
    
  9. service实现类

    @Service
    public class LoginServiceImpl implements LoginService {
    
        private final AuthenticationManager authenticationManager;
        private final RedisTemplate<String, LoginUser> redisTemplate;
    
        public LoginServiceImpl(AuthenticationManager authenticationManager, RedisTemplate<String, LoginUser> redisTemplate) {
            this.authenticationManager = authenticationManager;
            this.redisTemplate = redisTemplate;
        }
    
        @Override
        public RequestResult login(Users users) {
            // authenticationManager authenticate 进行用户认证
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(users.getUsername(), users.getPassword());
            Authentication authenticate = authenticationManager.authenticate(authenticationToken);
            // 认证失败
            if (Objects.isNull(authenticate)) {
                throw new UsernameNotFoundException("用户名或密码错误!!!");
            }
            // 认证成功,生成jwt
            LoginUser user = (LoginUser) authenticate.getPrincipal();
            HashMap<String, Object> jwtMap = new HashMap<>();
            jwtMap.put("userid", user.getUsers().getId());
            String token = JWTUtil.createToken(jwtMap, "123456".getBytes(StandardCharsets.UTF_8));
            // 存入redis userid作为key
            ValueOperations<String, LoginUser> idToken = redisTemplate.opsForValue();
            idToken.set("login:"+user.getUsers().getId(), user);
            // 返回结果
            HashMap<String, String> resultJson = new HashMap<>();
            resultJson.put("token", token);
            return new RequestResult(200, "登陆成功", resultJson);
        }
    }
    
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        private final UsersMapper userMapper;
    
        public UserDetailsServiceImpl(UsersMapper userMapper) {
            this.userMapper = userMapper;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            QueryWrapper<Users> queryWrapper = new QueryWrapper<Users>().eq("username", username);
            Users user = userMapper.selectOne(queryWrapper);
    
            if (user == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
    
            return new LoginUser(user);
        }
    }
    
  10. controller层

    @RestController
    @RequestMapping("/test")
    public class TestSpringSecurityController {
    
        private final LoginService loginService;
    
        public TestSpringSecurityController(LoginService loginService) {
            this.loginService = loginService;
        }
    
        @GetMapping("/hello")
        public String hello() {
            return "Hello SpringSecurity!";
        }
    
        @GetMapping("/index")
        public String index() {
            return "index!";
        }
    
        @PostMapping("/login")
        public RequestResult login(@RequestBody Users user) {
            return loginService.login(user);
        }
    
    }
    
  11. Security配置

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 关闭csrf
            http.csrf().disable()
                    // 不通过Session获取SecurityContext
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and().authorizeRequests()
                    // 对于登录接口 允许匿名访问
                    .antMatchers("/test/login").anonymous()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
        }
    
        @Bean
        @Override
        protected AuthenticationManager authenticationManager() throws Exception {
            return super.authenticationManager();
        }
    }
    
  12. 测试

    Spring Security + jwt入门_第6张图片

认证过滤器

  1. 创建jwt认证过滤器

    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        private final RedisTemplate<String, LoginUser> redisTemplate;
    
        public JwtAuthenticationTokenFilter(RedisTemplate<String, LoginUser> redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
            String token = request.getHeader("token");
            if (!StringUtils.hasText(token)) {
                // 登录请求或非法请求
                filterChain.doFilter(request, response);
                return;
            }
    
            // 验证token是否合法
            try {
                JWTUtil.verify(token, "123456".getBytes(StandardCharsets.UTF_8));
            } catch (Exception e) {
                // TODO: 2022/8/28 需要捕获异常
                throw new JWTException("token不合法!!");
            }
    
            // 解析token
            JWT jwt = JWTUtil.parseToken(token);
            String userid = jwt.getPayload("userid").toString();
    
            // redis获取用户信息
            ValueOperations<String, LoginUser> loginUserValue = redisTemplate.opsForValue();
            LoginUser loginUser = loginUserValue.get("login:" + userid);
    
            if(Objects.isNull(loginUser)){
                // TODO: 2022/8/28 需要捕获异常
                throw new UsernameNotFoundException("用户未登录!!");
            }
    
            // 存入SecurityContextHolder
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    
            // 验证通过放行
            filterChain.doFilter(request, response);
        }
    }
    
  2. 配置Security

    @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 把token校验过滤器添加到过滤器链中
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }
    

退出登录

  1. controller层

    @RestController
    @RequestMapping("/test")
    public class TestSpringSecurityController {
    
        private final LoginService loginService;
    
        public TestSpringSecurityController(LoginService loginService) {
            this.loginService = loginService;
        }
    
        @PutMapping("/logout")
        public RequestResult logout() {
            return loginService.logout();
        }
    
    }
    
  2. service层

    public interface LoginService {
        RequestResult login(Users user);
        RequestResult logout();
    }
    
  3. service实现类

    @Service
    public class LoginServiceImpl implements LoginService {
    
        private final AuthenticationManager authenticationManager;
        private final RedisTemplate<String, LoginUser> redisTemplate;
    
        public LoginServiceImpl(AuthenticationManager authenticationManager, RedisTemplate<String, LoginUser> redisTemplate) {
            this.authenticationManager = authenticationManager;
            this.redisTemplate = redisTemplate;
        }
    
        @Override
        public RequestResult logout() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            redisTemplate.delete("login:" + loginUser.getUsers().getId());
            return new RequestResult(200, "登出成功!");
        }
    }
    

4. 授权

授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。

RBAC权限模型

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

Spring Security + jwt入门_第7张图片

从数据库查询权限信息

  1. sql建表语句

    -- user用户表
    CREATE TABLE `user` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
      `username` varchar(255) NOT NULL COMMENT '用户名',
      `password` varchar(255) NOT NULL COMMENT '用户密码',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
    
    INSERT INTO `user` VALUES (1, 'admin', '$2a$10$Bvoup5Tzzkm6KWYvZEvJ5.AUFAOwbuq25mB7u8NGJq.GQAIcaKefK');
    INSERT INTO `user` VALUES (2, 'user', '$2a$10$U9Ww9laNXdf/f36mS8geoOb.AQmMUJ2GONlYmSdM2vcoxAvvHQohG');
    INSERT INTO `user` VALUES (3, 'test', '$2a$10$ezFcaSu3yTHrZ0MbStNa/e6upQ1fVIss8BMJw54pKBpgv4lB3/RwS');
    
    -- menu菜单表
    CREATE TABLE `menu` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜单id',
      `menu_name` varchar(64) NOT NULL COMMENT '权限名',
      `perm_key` varchar(100) NOT NULL COMMENT '权限标识',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
    
    INSERT INTO `menu` VALUES (1, '访问index', 'system:index:list');
    INSERT INTO `menu` VALUES (2, '访问hello', 'system:hello:list');
    
    -- role角色表
    CREATE TABLE `role` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色id',
      `name` varchar(128) NOT NULL COMMENT '角色名',
      `role_key` varchar(100) NOT NULL COMMENT '角色标识',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
    
    INSERT INTO `role` VALUES (1, '管理员', 'admin');
    INSERT INTO `role` VALUES (2, '普通用户', 'user');
    
    -- user_role用户角色关联表
    CREATE TABLE `user_role` (
      `user_id` bigint(200) NOT NULL COMMENT '用户id',
      `role_id` bigint(200) NOT NULL COMMENT '角色id',
      PRIMARY KEY (`user_id`,`role_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT '用户角色关联表';
    
    INSERT INTO `user_role` VALUES (1, 1);
    INSERT INTO `user_role` VALUES (2, 2);
    INSERT INTO `user_role` VALUES (3, 2);
    
    -- role_menu角色权限关联表
    CREATE TABLE `role_menu` (
      `role_id` bigint(200) NOT NULL COMMENT '角色id',
      `menu_id` bigint(200) NOT NULL COMMENT '菜单id',
      PRIMARY KEY (`role_id`,`menu_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限关联表';
    
    INSERT INTO `role_menu` VALUES (1, 1);
    INSERT INTO `role_menu` VALUES (1, 2);
    INSERT INTO `role_menu` VALUES (2, 2);
    
  2. 创建实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @TableName("menu")
    public class Menus {
    
        @TableId("id")
        private Long id;
        @TableField("menu_name")
        private String menuName;
        @TableField("perm_key")
        private String permKey;
    
    }
    
  3. Mapper层

    @Mapper
    public interface MenusMapper extends BaseMapper<Menus> {
    
        List<String> selectPermsByUserId(Long userid);
    
    }
    
  4. mapper映射xml文件

    
    DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="pers.itqiqi.springSecurity.mapper.MenusMapper">
    
        <select id="selectPermsByUserId" resultType="java.lang.String">
            SELECT
                m.perm_key
            FROM
                `user` u
                LEFT JOIN user_role ur ON ur.user_id = u.id
                LEFT JOIN role_menu rm ON rm.role_id = ur.role_id
                LEFT JOIN menu m ON m.id = rm.menu_id
            WHERE
                u.id = #{userid}
        select>
    mapper>
    
  5. 修改UserDetailsService实现类

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        private final UsersMapper userMapper;
        private final MenusMapper menusMapper;
    
        public UserDetailsServiceImpl(UsersMapper userMapper, MenusMapper menusMapper) {
            this.userMapper = userMapper;
            this.menusMapper = menusMapper;
        }
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            QueryWrapper<Users> queryWrapper = new QueryWrapper<Users>().eq("username", username);
            Users user = userMapper.selectOne(queryWrapper);
    
            if (user == null) {
                throw new UsernameNotFoundException("用户不存在");
            }
    
            // 数据库拿到权限list
            List<String> permsList = menusMapper.selectPermsByUserId(user.getId());
            List<GrantedAuthority> authorityList = AuthorityUtils.createAuthorityList(ArrayUtil.toArray(permsList, String.class));
            return new LoginUser(user, authorityList);
        }
    }
    
  6. 启用全局方法安全

    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
  7. controller接口升级

    @RestController
    @RequestMapping("/test")
    public class TestSpringSecurityController {
    
        private final LoginService loginService;
    
        public TestSpringSecurityController(LoginService loginService) {
            this.loginService = loginService;
        }
    
        @GetMapping("/hello")
        @PreAuthorize("hasAnyAuthority('system:hello:list')")
        public String hello() {
            return "Hello SpringSecurity!";
        }
    
        @GetMapping("/index")
        @PreAuthorize("hasAnyAuthority('system:index:list')")
        public String index() {
            return "index!";
        }
    
        @PostMapping("/login")
        public RequestResult login(@RequestBody Users user) {
            return loginService.login(user);
        }
    
        @PutMapping("/logout")
        public RequestResult logout() {
            return loginService.logout();
        }
    
    }
    
  8. 测试

    Spring Security + jwt入门_第8张图片

    Spring Security + jwt入门_第9张图片

5.自定义失败处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

  1. 自定义实现类

    @Component
    @Slf4j
    // 授权过程中出现的异常
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            RequestResult requestResult = new RequestResult(HttpStatus.FORBIDDEN.value(), "权限不足");
            log.warn("授权过程中出现的异常: {}",accessDeniedException.getMessage());
            writer.print(JSONUtil.toJsonStr(requestResult));
        }
    }
    
    @Component
    @Slf4j
    // 认证过程中出现的异常
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            response.setCharacterEncoding("utf-8");
            response.setContentType("application/json; charset=utf-8");
            PrintWriter writer = response.getWriter();
            RequestResult requestResult = new RequestResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
            log.warn("认证过程中出现的异常: {}",authException.getMessage());
            writer.print(JSONUtil.toJsonStr(requestResult));
        }
    
    }
    
  2. 配置SpringSecurity

    http.exceptionHandling()
    	// 权限不足
        .accessDeniedHandler(accessDeniedHandler)
        // 认证异常
        .authenticationEntryPoint(authenticationEntryPoint);
    

6.跨域

浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

  1. 对SpringBoot配置

    @Configuration
    public class CorsConfig implements WebMvcConfigurer {
    
        @Override
        public void addCorsMappings(CorsRegistry registry) {
            // 设置允许跨域的路径
            registry.addMapping("/**")
                    // 设置允许跨域请求的域名
                    .allowedOriginPatterns("*")
                    // 是否允许cookie
                    .allowCredentials(true)
                    // 设置允许的请求方式
                    .allowedMethods("GET", "POST", "DELETE", "PUT")
                    // 设置允许的header属性
                    .allowedHeaders("*")
                    // 跨域允许时间
                    .maxAge(3600);
        }
    }
    
  2. 开启SpringSecurity的跨域访问

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

7.小知识

其它权限校验方法

hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。

@PreAuthorize("hasAnyAuthority('admin','test','system:hello:list')")
public String hello(){
    return "hello";
}

hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

@PreAuthorize("hasRole('system:hello:list')")
public String hello(){
    return "hello";
}

hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。

@PreAuthorize("hasAnyRole('admin','system:hello:list')")
public String hello(){
    return "hello";
}

自定义权限校验方法

可以定义权限校验方法,在@PreAuthorize注解中使用。

  1. 编写权限校验类

    @Component("ex")
    public class ExpressionRoot {
    
        public boolean hasAuthority(String authority){
            //获取当前用户的权限
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            LoginUser loginUser = (LoginUser) authentication.getPrincipal();
            List<String> permissions = loginUser.getPermissions();
            //判断用户权限集合中是否存在authority
            return permissions.contains(authority);
        }
       
    }
    
  2. 在SPEL表达式中使用 @ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法

    @RequestMapping("/hello")
    @PreAuthorize("@ex.hasAuthority('system:hello:list')")
    public String hello(){
        return "hello";
    }
    

基于配置的权限控制

可以在配置类中使用使用配置的方式对资源进行权限控制。

.antMatchers("/test/hello").hasAuthority("system:hello:list")

CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

你可能感兴趣的:(java,spring,java,后端)