zoukankan      html  css  js  c++  java
  • SpringSecurity 整合JWT实现无状态登陆

    SpringSecurity 整合JWT实现无状态登陆

    案例使用SpringBoot作为基础框架快速集成JWT

    1.添加启动依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <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.0</modelVersion>
    
        <groupId>cn.blogsx</groupId>
        <artifactId>springboot_security_jwt</artifactId>
        <version>1.0-SNAPSHOT</version>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.0.1.RELEASE</version>
        </parent>
    
        <dependencies>
            <!-- web功能起步依赖-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--Spring Security依赖包-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <!-- jwt -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.7.0</version>
            </dependency>
            <!--  mybatis依赖-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>1.3.2</version>
            </dependency>
            <!-- druid数据库连接池-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid</artifactId>
                <version>1.1.9</version>
            </dependency>
            <!-- mysql驱动-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <scope>runtime</scope>
            </dependency>
        </dependencies>
    </project>
    

    2.添加配置文件并配置基本信息

    server.port=8080
    
    # 数据库连接相关配置
    spring.datasource.url=jdbc:mysql:///springsecurity?characterEncoding=utf8&useSSL=true
    spring.datasource.driverClassName=com.mysql.jdbc.Driver
    spring.datasource.username=root
    spring.datasource.password=root
    
    # MyBatis注解形式扫描实体类路径
    mybatis.type-aliases-package=cn.blogsx.entity
    
    # MyBatis XML形式配置文件路径
    mybatis.config-locations=classpath:mybatis/mybatis-config.xml
    mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
    
    # 配置Jwt密钥
    jwt.secret=Alex
    

    3.创建数据库表信息

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;
    
    -- ----------------------------
    -- Table structure for role
    -- ----------------------------
    DROP TABLE IF EXISTS `role`;
    CREATE TABLE `role`  (
      `id` int(11) NOT NULL,
      `name` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
      `nameZh` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of role
    -- ----------------------------
    INSERT INTO `role` VALUES (1, 'ROLE_dba', '数据库管理员');
    INSERT INTO `role` VALUES (2, 'ROLE_admin', '系统管理员');
    INSERT INTO `role` VALUES (3, 'ROLE_user', '用户');
    
    -- ----------------------------
    -- Table structure for user
    -- ----------------------------
    DROP TABLE IF EXISTS `user`;
    CREATE TABLE `user`  (
      `id` int(11) NOT NULL,
      `username` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
      `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
      `enabled` tinyint(1) NULL DEFAULT NULL,
      `locked` tinyint(1) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
    
    -- ----------------------------
    -- Records of user
    -- ----------------------------
    INSERT INTO `user` VALUES (1, 'root', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
    INSERT INTO `user` VALUES (2, 'admin', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
    INSERT INTO `user` VALUES (3, 'alex', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
    
    -- ----------------------------
    -- Table structure for user_role
    -- ----------------------------
    DROP TABLE IF EXISTS `user_role`;
    CREATE TABLE `user_role`  (
      `id` int(11) NOT NULL,
      `uid` int(11) NULL DEFAULT NULL,
      `rid` int(11) NULL DEFAULT NULL,
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Fixed;
    
    -- ----------------------------
    -- Records of user_role
    -- ----------------------------
    INSERT INTO `user_role` VALUES (1, 1, 1);
    INSERT INTO `user_role` VALUES (2, 1, 2);
    INSERT INTO `user_role` VALUES (3, 2, 2);
    INSERT INTO `user_role` VALUES (4, 3, 3);
    
    SET FOREIGN_KEY_CHECKS = 1;
    
    

    4.新建测试接口

    接口

    @RestController
    public class UserController {
    
        @RequestMapping("/hello")
        public Object hello() {
            String str  = "hello";
            return str;
        }
        //接口调用前判断是否又admin角色
        @PreAuthorize("hasRole('admin')") //此处使用注解实现方法级的安全,也可以在SecurityConfig中统一配置
        @RequestMapping("/admin/hello")
        public Object adminHello() {
            String str  = "/admin/hello";
            return str;
        }
    }
    

    Bean

    package cn.blogsx.entity;
    
    public class Role {
        private Integer id;
        private String name;
        private String nameZh;
    
       //省略getter和setter及构造方法
    }
    
    //user对象需要实现UserDetails接口才能在UserDetailsServiceImpl中的loadUserByUsername方法使用
    ublic class User implements UserDetails {
        private Integer id;
        private String username;
        private String password;
        private Boolean enabled;
        private Boolean locked;
        private List<Role> roles;
    
        public User() {
        }
    
        public User(Integer id, String username, String password, Boolean enabled, Boolean locked, List<Role> roles) {
            this.id = id;
            this.username = username;
            this.password = password;
            this.enabled = enabled;
            this.locked = locked;
            this.roles = roles;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            List<SimpleGrantedAuthority> authorities = new ArrayList<>();
            for (Role role:roles){
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return password;
        }
    
        @Override
        public String getUsername() {
            return username;
        }
    
        @Override
        public boolean isAccountNonExpired() {
            return true;
        }
    
        @Override
        public boolean isAccountNonLocked() {
            return !locked;
        }
    
        @Override
        public boolean isCredentialsNonExpired() {
            return true;
        }
    
        @Override
        public boolean isEnabled() {
            return enabled;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public List<Role> getRoles() {
            return roles;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    }
    
    

    5.创建SecurityConfig配置,配置接口安全策略

    @Configuration
    //使用 @PreAuthorize("hasRole('admin')") 方法级安全注解时必须使用该注解声明才能使用
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        private JwtConfig jwtConfig;
    
        @Autowired
        UserDetailsServiceImpl userServiceImpl;
    
        /**
         * 配置SpringSecurity 加密方式
         * @return 加密对象
         */
        @Bean
        PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            //使用数据库查询用户作用认证数据源
            auth.userDetailsService(userServiceImpl);
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //由于下文的jwt过滤器不使用spring来管理,故jwt所需配置需要在注册时设置配置文件值
            JwtLoginFilter jwtLoginFilter = new JwtLoginFilter("/login", authenticationManager());
            jwtLoginFilter.setSecret(jwtConfig.getSecret());
            JwtFilter jwtFilter = new JwtFilter(jwtConfig.getSecret());
            http.authorizeRequests()
                    //配置统一拦截路径,也可使用 @PreAuthorize("hasRole('admin')")类似注解灵活配置	
    //                .antMatchers("/hello")
    //                .hasRole("user")
    //                .antMatchers("/admin")
    //                .hasRole("admin")
                    //配置登陆接口
                    .antMatchers(HttpMethod.POST, "/login")
                    .permitAll()
                    .anyRequest().authenticated()
                    .and()
                    //配置jwt过滤器
                    .addFilterBefore(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class)
                    .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
                    .csrf().disable();
    
        }
    }
    

    jwtconfig

    @Configuration
    public class JwtConfig {
        @Value("${jwt.secret}") //使用该注解一定要是被spring管理的类才能注入值,过滤器或监听器无法使用(因为spring中的类加载顺序是:listener->filter->servlet)
        private String secret; //jwt密钥
    
        public String getSecret() {
            return secret;
        }
    
        public void setSecret(String secret) {
            this.secret = secret;
        }
    }
    

    6.配置JWT过滤器

    jwt登陆过滤器(用于在登陆时颁发toekn)

    public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
    
        private String secret; //jwt密钥
    
        public String getSecret() {
            return secret;
        }
    
        public void setSecret(String secret) {
            this.secret = secret;
        }
    
        public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
            super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
            setAuthenticationManager(authenticationManager);
        }
    
        /**
         * 配置Jwt登陆拦截器
         * @param req
         * @param httpServletResponse
         * @return
         * @throws AuthenticationException
         * @throws IOException
         */
        @Override
        public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException {
            User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
            return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
        }
    
        /**
         * 登陆成功后返回Token
         * @param request
         * @param resp
         * @param chain
         * @param authResult
         * @throws IOException
         * @throws ServletException
         */
        @Override
        protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
            Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();//获取登录用户的角色
            StringBuffer sb = new StringBuffer();
            for (GrantedAuthority authority : authorities) {
                sb.append(authority.getAuthority()).append(",");
            }
            String jwt = Jwts.builder()
                    .claim("authorities", sb)
                    .setSubject(authResult.getName())
                    .setExpiration(new Date(System.currentTimeMillis() +  60 * 60 * 1000))
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
            Map<String, String> map = new HashMap<>();
            map.put("token", jwt);
            map.put("msg", "登录成功");
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        }
    
        /**
         * 登陆失败,返回json提示信息
         * @param req
         * @param resp
         * @param failed
         * @throws IOException
         * @throws ServletException
         */
        @Override
        protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
            Map<String, String> map = new HashMap<>();
            map.put("msg", "登录失败");
            resp.setContentType("application/json;charset=utf-8");
            PrintWriter out = resp.getWriter();
            out.write(new ObjectMapper().writeValueAsString(map));
            out.flush();
            out.close();
        }
    }
    

    jwt过滤器(用于每次请求过滤校验token的合法性等)

    /**
     * 配置登陆后每次拦截jwt检验token合法性拦截器,无需查询数据库
     */
    public class JwtFilter extends GenericFilterBean {
    
        private String secret;
    
        public JwtFilter(String secret) {
            this.secret = secret;
        }
    
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            HttpServletRequest req = (HttpServletRequest) servletRequest;
            String jwtToken = req.getHeader("authorization");
            Jws<Claims> jws=null;
            try {
                jws= Jwts.parser().setSigningKey(secret)
                        .parseClaimsJws(jwtToken.replace("Bearer", ""));
            }catch (ExpiredJwtException e) {
                //Token已过期,返回提示信息
                Map<String, String> map = new HashMap<>();
                map.put("msg", "Token已过期,请重新登陆");
                servletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = servletResponse.getWriter();
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
                return;
            } catch (SignatureException e){
                //Token签名异常,返回提示信息
                Map<String, String> map = new HashMap<>();
                map.put("msg", "Token签名异常");
                servletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = servletResponse.getWriter();
                out.write(new ObjectMapper().writeValueAsString(map));
                out.flush();
                out.close();
                return;
            }
            Claims claims = jws.getBody();
            String username = claims.getSubject();
            List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
            SecurityContextHolder.getContext().setAuthentication(token);
            filterChain.doFilter(servletRequest,servletResponse);
        }
    }
    
    

    工程地址:https://gitee.com/sixudev/SpringBootStudy

  • 相关阅读:
    AODH: ALARM EVENTS IN OPENSTACK
    OpenStack企业私有云新需求(1):Nova 虚机支持 GPU
    How to use the ZooKeeper driver for ServiceGroup in OpenStack Nova
    keystone DB in devstack
    用外部物理路由器时使用Neutron dhcp-agent提供的metadata服务(by quqi99)
    调试OpenStack时遇到的主要问题(by quqi99)
    09 算数运算符
    08 常量
    07 值传递和地址传递
    06 指针入门
  • 原文地址:https://www.cnblogs.com/sxblog/p/14108687.html
Copyright © 2011-2022 走看看