SpringBoot + Swagger + SpringSec

作者: 秃头猿猿 | 来源:发表于2019-05-29 17:19 被阅读269次

    SpringBoot + Swagger + SpringSecurity + Jwt做登陆认证

    @author wangzh@briup.com

    1.前置知识

     熟悉 SpringBoot
    
     了解 SpringSecurity
    
     熟悉 jwt
    

    2.一些没用的废话

    项目最近用到了 SpringSecutiry + JWT做登陆验证,之所以会使用这种登陆验证,因为前后端分离的情况下,服务器并不只是只有浏览器去访问,还包括了其他的设备,比如说手机,Pad,小程序等。如果采用的还是基于Session登陆,那么手机,Pad,小程序等并没有像浏览器一样存在着cookie,因此可以采用这种登陆认证方式。

    3.准备工作

    3.1 项目环境搭建

    创建SpringBoot项目,并添加 JWT 依赖和SpringSecurity依赖,具体pom.xml内容如下:

    <?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>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.3.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.briup</groupId>
        <artifactId>security-demo</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>security-demo</name>
        <description>Demo project for Spring Boot</description>
    
        <properties>
            <java.version>1.8</java.version>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
        
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.0.0</version>
            </dependency>
            
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            
            <!-- swagger api文档 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>2.6.1</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>2.6.1</version>
            </dependency>
            
            <!-- 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.9.0</version>
            </dependency> 
    
            <!-- 工具类依赖 -->
            <dependency>
                <groupId>commons-lang</groupId>
                <artifactId>commons-lang</artifactId>
                <version>2.6</version>
            </dependency>
            
            
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    

    3.2 application.properties内容为:

    server.port=8888
    spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/demo
    spring.datasource.username=root
    spring.datasource.password=root
    
    mybatis.mapper-locations= classpath:/mapper/**/*Mapper.xml
    

    3.3 创建表(mysql)

    -- ----------------------------
    -- Table structure for customer
    -- ----------------------------
    DROP TABLE IF EXISTS `customer`;
    CREATE TABLE `customer` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) DEFAULT NULL,
      `password` varchar(255) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Records of customer
    -- ----------------------------
    INSERT INTO `customer` VALUES ('1', 'admin', 'admin');
    
    -- ----------------------------
    -- Table structure for roles
    -- ----------------------------
    DROP TABLE IF EXISTS `roles`;
    CREATE TABLE `roles` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(255) DEFAULT NULL,
      `customer_id` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`),
      KEY `roles_customer_id_fk` (`customer_id`),
      CONSTRAINT `roles_customer_id_fk` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
    
    -- ----------------------------
    -- Records of roles
    -- ----------------------------
    INSERT INTO `roles` VALUES ('1', '管理员', '1');
    
    

    3.4 根据表创建映射文件与映射接口,POJO类

    Customer类内容如下:

    package com.briup.security.bean;
    
    import java.io.Serializable;
    import java.util.List;
    
    public class Customer implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private long id;
        private String name;
        private String password;
        private List<Role> roles;
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public List<Role> getRoles() {
            return roles;
        }
    
        public void setRoles(List<Role> roles) {
            this.roles = roles;
        }
    
    }
    

    Role类内容如下:

    package com.briup.security.bean;
    
    import java.io.Serializable;
    
    public class Role implements Serializable {
    
        private static final long serialVersionUID = -2158194219185524323L;
        
        private long id;
        private String name;
    
        private Customer customer;
    
        public long getId() {
            return id;
        }
    
        public void setId(long id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Customer getCustomer() {
            return customer;
        }
    
        public void setCustomer(Customer customer) {
            this.customer = customer;
        }
        
    }
    
    

    CustomerMapper 映射接口为:

    package com.briup.security.mapper;
    
    import java.util.List;
    
    import com.briup.security.bean.Customer;
    
    public interface CustomerMapper {
        
        /**
         * 根据名字 查找用户信息
         * @param name
         * @return
         */
        Customer selectByName(String name);
        
        /**
         * 查询所有的用户信息
         * @return
         */
        List<Customer> selectAll();
    
    }
    
    

    RoleMapper 映射接口为:

    package com.briup.security.mapper;
    
    import java.util.List;
    
    import com.briup.security.bean.Role;
    
    public interface RoleMapper {
    
        /**
         * 根据用户id查询用户所有的角色
         * @param customerId
         * @return
         */
        List<Role> selectAllByCustomerId(Integer customerId);
        
    }
    
    

    CustomerMapper.xml 映射文件为:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.briup.security.mapper.CustomerMapper">
        <select id="selectByName" resultType="com.briup.security.bean.Customer">
            select * from customer where name = #{name}
        </select>
        
        <select id="selectAll" resultType="com.briup.security.bean.Customer">
            select * from customer
        </select>
    </mapper>
    

    RoleMapper.xml 映射文件为:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
      PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.briup.security.mapper.RoleMapper">
        <select id="selectAllByCustomerId" resultType="com.briup.security.bean.Role">
            select * from roles where customer_id = #{id}
        </select>
    </mapper>
    

    3.5 编写service接口与实现类

    ICustomerService 接口内容为:

    package com.briup.security.service;
    
    import java.util.List;
    
    import com.briup.security.bean.Customer;
    
    public interface ICustomerService {
        
        /**
         * 根据 名字查询
         * @param name
         * @return
         */
        Customer findByName(String name);
        
        /**
         * 查询所有
         * @return
         */
        List<Customer> findAll();
    
    }
    
    

    CustomerServiceImpl 实现类如下:

    package com.briup.security.service.impl;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.briup.security.bean.Customer;
    import com.briup.security.mapper.CustomerMapper;
    import com.briup.security.service.ICustomerService;
    
    @Service
    public class CustomerServiceImpl implements ICustomerService {
    
        @Autowired
        private CustomerMapper customerMapper;
        
        @Override
        public Customer findByName(String name) {
            return customerMapper.selectByName(name);
        }
    
        @Override
        public List<Customer> findAll() {
            return customerMapper.selectAll();
        }
    
    }
    
    

    IRoleService 接口如下:

    package com.briup.security.mapper;
    
    import java.util.List;
    
    import com.briup.security.bean.Role;
    
    public interface RoleMapper {
    
        /**
         * 根据用户id查询用户所有的角色
         * @param customerId
         * @return
         */
        List<Role> selectAllByCustomerId(long customerId);
        
    }
    
    

    RoleServiceImpl 内容如下

    package com.briup.security.service.impl;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import com.briup.security.bean.Role;
    import com.briup.security.mapper.RoleMapper;
    import com.briup.security.service.IRoleService;
    
    @Service
    public class RoleServiceImpl implements IRoleService {
    
        @Autowired
        private RoleMapper roleMapper;
        
        
        @Override
        public List<Role> findAllByCustomerId(long id) {
            return roleMapper.selectAllByCustomerId(id);
        }
    
    }
    
    

    4.编写controller测试所写功能

    CustomerController内容如下:

    package com.briup.security.web.controller;
    
    import java.util.List;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.briup.security.bean.Customer;
    import com.briup.security.service.ICustomerService;
    
    @RestController
    @RequestMapping("/customer")
    public class CustomerController {
    
        @Autowired
        private ICustomerService customerService;
        
        
        @GetMapping("/getCustomer")
        public Customer getCustomerByName(String name) {
            return customerService.findByName(name);
        }
        
        
    
        @GetMapping("/getAllCustomer")
        public List<Customer> getAllCustomer() {
            return customerService.findAll();
        }
        
        
    }
    

    在启动类加上MapperScanner注解,启动,具体代码如下:

    package com.briup.security;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan("com.briup.security.mapper")
    public class SecurityDemoApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SecurityDemoApplication.class, args);
        }
    
    }
    

    访问结果如下:

    http://localhost:8888/customer/getAllCustomer

    image.png

    这种访问方式,在前后端分离的情况下,不方便前端人员去对接java后端人员编写的接口,因此我们使用swagger去暴露我们的服务

    5.整合swagger

    5.1 添加swagger依赖,在上面的pom.xml中已经添加,下面只是罗列出来,不需要重复添加

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.6.1</version>
    </dependency>
    

    5.2 添加swagger2的配置类 Swagger2Config

    package com.briup.security.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    
    @Configuration
    @EnableSwagger2
    public class Swagger2Config {
        
        
        @Bean
        public Docket createRestApi() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.briup.security.web"))
                    .paths(PathSelectors.any())
                    .build();
        }
    
    
    
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("spring-security-demo")
                    .description("昆山杰普软件科技有限公司,http://www.briup.com")
                    .termsOfServiceUrl("http://www.briup.com")
                    .version("1.0")
                    .build();
        }
    }
    
    
    

    5.3 重新启动项目,并访问如下网址:

    http://localhost:8888/swagger-ui.html

    image.png
    image.png

    经过上面的例子发现,所有的服务在没有安全认证的情况下都可以访问,因此我们需要添加安全认证,采用SpringSecurity + jwt去进行安全认证,至于为什么采用这种形式,在上文已经解释了使用session保存用户信息所带来的问题,这里不再赘述.

    6.整合Security + JWT

    6.1 导入security与JWT依赖,在上面的pom.xml中已经添加,下面只是罗列出来,不需要重复添加

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.0</version>
    </dependency> 
    

    6.2 添加Security的配置类,SecurityConfig具体代码如下:

    /**
     * Project Name:auth2
     * File Name:WebSecurityConfig.java
     * Package Name:com.briup.apps.auth2.config
     * Date:2018年9月17日上午10:23:44
     * Copyright (c) 2018, chenzhou1025@126.com All Rights Reserved.
     *
    */
    
    package com.briup.security.config;
    
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    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.config.http.SessionCreationPolicy;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.util.DigestUtils;
    
    /**
     * ClassName:WebSecurityConfig <br/>
     * Function: security 配置类 <br/>
     * Date: 2018年9月17日 上午10:23:44 <br/>
     *
     * @author wangzh
     * @version
     * @since JDK 1.8
     * @see
     */
    @Configuration
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Autowired
        @Qualifier("userDetailServiceImpl")
        private UserDetailsService userDetailService;
    
        @Bean
        public PasswordEncoder getPasswordEncoderBean() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public JwtAuthenticationTokenFilter getauthenticationTokenFilterBean() {
            return new JwtAuthenticationTokenFilter();
        }
    
        @Bean
        public LoginSuccessHandler getLoginSuccessHandler() {
            return new LoginSuccessHandler();
        }
    
        @Bean
        public LoginFailHandler getLoginFailHandler() {
            return new LoginFailHandler();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/authenticaion/login")
                .loginProcessingUrl("/authentication/form")
                .successHandler(getLoginSuccessHandler())
                .failureHandler(getLoginFailHandler())
                .and()
                .csrf().disable() //使用jwt,不需要csrf
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //基于token,不需要session
                .and()
                .authorizeRequests()
                // 设置允许访问的资源
                .antMatchers("/authenticaion/login").permitAll()
                // 设置允许访问的资源
                .antMatchers("/webjars/**").permitAll()
                .antMatchers(
                            "/v2/api-docs",
                            "/swagger-resources",
                            "/swagger-resources/**",
                            "/configuration/ui",
                            "/configuration/security",
                            "/swagger-ui.html/**",
                            "/webjars/**"
    
                    ).permitAll()
                    .anyRequest().authenticated();
    
             // 禁用缓存
            http.headers().cacheControl();
    
            // 添加JWT filter
            http.addFilterBefore(getauthenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
        }
    
    
    }
    
    

    userDetailService: 用来实现自定义登陆逻辑

    BCryptPasswordEncoder: 用来实现密码加密处理(这是security已经实现好了,我们不需要去实现)

    JwtAuthenticationTokenFilter: 用来实现token验证

    JwtTokenUtils: jwt工具类

    LoginSuccessHandler: 登陆成功后的处理器

    LoginFailHandler: 登陆失败后的处理器

    /authenticaion/login : 登陆时访问的地址(会在SecurityController实现)

    /authentication/form: 登陆的url地址(会在LoginController实现))

    接下来让我们挨个实现

    6.3 UserDetailService 这是一个security接口,需要我们自己写实现类去实现这个接口 UserDetailServiceImpl内容如下:

    package com.briup.security.service.impl;
    
    import java.util.ArrayList;
    import java.util.List;
    
    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.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
    import com.briup.security.bean.Customer;
    import com.briup.security.bean.Role;
    import com.briup.security.service.ICustomerService;
    import com.briup.security.service.IRoleService;
    
    /**
     * 
     * <p>
     *      security 自定义登陆逻辑类
     *      用来做登陆认证,验证用户名与密码
     * </p>
     * 
     * @author wangzh
     *
     */
    @Component("userDetailServiceImpl")
    public class UserDetailServiceImpl implements UserDetailsService {
        
        @Autowired
        private ICustomerService customerService;
    
        @Autowired
        private IRoleService roleService;
        
        @Autowired
        private PasswordEncoder passwordEncoder;
        
    
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
            // 根据用户名去查找用户信息
            Customer customer = customerService.findByName(username);
            
            if(customer == null) {
                throw new UsernameNotFoundException(String.format("Not user Found with '%s'", username));
            }
    
            // 根据用户id查询角色
            List<Role> roles = roleService.findAllByCustomerId(customer.getId());
    
            return new User(customer.getName(),passwordEncoder.encode(customer.getPassword()),getGrantedAuthority(roles));
        }
    
        /*** 
         * @Description: 获取角色权限
         * @Param: [roles] 
         * @return: java.util.List<org.springframework.security.core.GrantedAuthority> 
         * @Author: wangzh
         * @Date: 2019/3/21 
         */
        private List<GrantedAuthority> getGrantedAuthority(List<Role> roles) {
            List<GrantedAuthority> authorities = new ArrayList<>(roles.size());
    
            for (Role role : roles) {
                authorities.add(new SimpleGrantedAuthority(role.getName()));
            }
            return authorities;
        }
    
    
    }
    
    

    6.4 JwtTokenUtils内容如下:

    package com.briup.security.util;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Set;
    
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.authority.AuthorityUtils;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    
    /**
     * @author : wangzh
     * @version V1.0
     * @Description: jwt工具类
     */
    public class JwtTokenUtils {
    
        public static final String TOKEN_HEADER = "Authorization";
    
        public static final String TOKEN_PREFIX = "Bearer ";
        /**
         * 密钥key
         */
        private static final String SECRET = "jwtsecurit";
    
        /**
         * JWT的发行人
         */
        private static final String ISS = "Kunshan Briup";
    
        /**
         * 自定义用户信息
         */
        private static final String ROLE_CLAIMS = "rol";
    
        /**
         * 过期时间是3600秒,既是1个小时
         */
        public static final long EXPIRATION = 3600L * 1000;
    
        /**
         * 选择了记住我之后的过期时间为7天
         */
        public static final long EXPIRATION_REMEMBER = 604800L * 1000;
    
        /**
         * 创建token
         * 
         * @param username
         *            登录名
         * @param roles
         *            用户角色信息
         * @param isRememberMe
         *            是否记住我
         * @return
         */
        public static String createToken(UserDetails details, boolean isRememberMe) throws CustomerException {
            // 如果选择记住我,则token的过期时间为
            long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
    
            HashMap<String, Object> map = new HashMap<>();
    
            map.put(ROLE_CLAIMS, details.getAuthorities()); // 角色名字
            return Jwts.builder().signWith(SignatureAlgorithm.HS512, SECRET) // 加密算法
                    .setClaims(map) // 自定义信息
                    .setIssuer(ISS) // jwt发行人
                    .setSubject(details.getUsername()) // jwt面向的用户
                    .setIssuedAt(new Date()) // jwt发行人
                    .setExpiration(new Date(System.currentTimeMillis() + expiration)) // key过期时间
                    .compact();
        }
    
        /**
         * 从token获取用户信息
         * 
         * @param token
         * @return
         */
        public static String getUsername(String token) throws CustomerException {
            return getTokenBody(token).getSubject();
        }
    
        /**
         * 从token中获取用户角色
         * 
         * @param token
         * @return
         */
        public static Set<String> getUserRole(String token) throws CustomerException {
            List<GrantedAuthority> userAuthorities = (List<GrantedAuthority>) getTokenBody(token).get(ROLE_CLAIMS);
            return AuthorityUtils.authorityListToSet(userAuthorities);
        }
    
        /**
         * 是否已过期
         * 
         * @param token
         * @return
         */
        public static boolean isExpiration(String token) throws CustomerException {
            return getTokenBody(token).getExpiration().before(new Date());
        }
    
        private static Claims getTokenBody(String token) throws CustomerException {
            return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
        }
    
        /**
         * 验证token
         * 
         * @param token
         * @param userDetails
         * @return
         */
        public static boolean validateToken(String token, UserDetails userDetails) throws CustomerException {
            User user = (User) userDetails;
            final String username = getUsername(token);
            return (username.equals(user.getUsername()) && isExpiration(token) == false);
        }
    
    }
    

    6.5 token校验过滤器

    package com.briup.security.web.filter;
    
    import java.io.IOException;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    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.web.authentication.WebAuthenticationDetails;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import com.briup.security.util.JwtTokenUtils;
    import com.briup.security.util.MessageUtil;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    /**
     * @program: paz
     * @description: token过滤器,用来验证token的有效性
     * @author: wangzh
     * @create: 2019-03-21 15:41
     */
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        @Autowired
        @Qualifier("userDetailServiceImpl")
        private UserDetailsService userDetailService;
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            String token = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
            if(token != null && StringUtils.startsWith(token, JwtTokenUtils.TOKEN_PREFIX)) {
                token = StringUtils.substring(token, JwtTokenUtils.TOKEN_PREFIX.length());
            } else {
                filterChain.doFilter(request, response);
                return;
            }
    
            try {
                String username = JwtTokenUtils.getUsername(token);
                if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                    /* 
                     *  注意:
                     *       这里代码不应该从数据库中去查,而是从缓存中根据token去查,目前只是做测试,无关紧要
                     *      如果是真正的项目实际开发需要增加缓存
                     */
                    UserDetails userDetails = userDetailService.loadUserByUsername(username);
                    
                    if (JwtTokenUtils.validateToken(token, userDetails)) {
                        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                                userDetails, null, userDetails.getAuthorities());
                        authentication.setDetails(new WebAuthenticationDetails(request));
    
    
                        SecurityContextHolder.getContext().setAuthentication(authentication);
                    }
    
                }
            } catch (Exception e) {
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(MessageUtil.error(401,"token已失效")));
                return;
            }
    
            filterChain.doFilter(request, response);
        }
    }
    
    

    6.6 登陆成功处理器 LoginSuccessHandler 内容如下:

    package com.briup.security.config;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    
    import com.briup.security.util.JwtTokenUtils;
    import com.briup.security.util.MessageUtil;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    /**
     * <p>
     *     登陆失败处理器
     * </p>
     * @Author: wangzh
     * @Date: 2019/3/21
     */
    public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    
        @Autowired
        @Qualifier("userDetailServiceImpl")
        private UserDetailsService userDetailsService;
        
        @Autowired
        private ObjectMapper objectMapper;
    
    
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=UTF-8");
            try {
                User details = (User) userDetailsService.loadUserByUsername(authentication.getName());
    
                String token = JwtTokenUtils.TOKEN_PREFIX  + JwtTokenUtils.createToken(details, false);
    
                // 重定向
                response.setHeader(JwtTokenUtils.TOKEN_HEADER, token);
                response.getWriter().write(objectMapper.writeValueAsString(MessageUtil.success(token)));
            } catch (Exception e) {
                response.getWriter().write(objectMapper.writeValueAsString(MessageUtil.error(401,"创建token失败,请与管理员联系")));
            }
    
        }
    
    }
    
    

    6.7 登陆失败处理器 LoginFailHandler 内容如下:

    package com.briup.security.config;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    
    import com.briup.security.util.MessageUtil;
    import com.fasterxml.jackson.databind.ObjectMapper;
    
    /**
     * <p>
     *     登陆失败处理器
     * </p>
     * @Author: wangzh
     * @Date: 2019/3/21
     */
    public class LoginFailHandler implements AuthenticationFailureHandler {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException exception) throws IOException, ServletException {
    
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(MessageUtil.error(401,"登陆失败:" + exception.getMessage())));
        }
    
    }
    
    

    SecurityController 内容如下:

    package com.briup.security.web.controller;
    
    import java.io.IOException;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang.StringUtils;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.web.DefaultRedirectStrategy;
    import org.springframework.security.web.RedirectStrategy;
    import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
    import org.springframework.security.web.savedrequest.RequestCache;
    import org.springframework.security.web.savedrequest.SavedRequest;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.briup.security.util.Message;
    import com.briup.security.util.MessageUtil;
    
    
    /**
     * @program: paz
     * @description: 发送请求,如果token为空,跳转到这个controller
     * @author: wangzh
     * @create: 2019-03-21 15:41
     */
    @RestController
    @RequestMapping("/authenticaion")
    public class SecurityController {
    
        private RequestCache requestCache = new HttpSessionRequestCache();
        
        private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
        
        /**
         * 当需要身份认证时,跳转到这里
         * 
         * @param request
         * @param response
         * @return
         * @throws IOException
         */
        @GetMapping("/login")
        @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
        public Message requireAuthentication(HttpServletRequest request, HttpServletResponse response)
                throws IOException {
    
            SavedRequest savedRequest = requestCache.getRequest(request, response);
    
            if (savedRequest != null) {
                String targetUrl = savedRequest.getRedirectUrl();
                if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                    // TODO 跳转到登陆页面
                    redirectStrategy.sendRedirect(request, response, "/login.html");
                }
            }
            return MessageUtil.error(401,"访问的服务需要身份认证,请引导用户到登录页");
        }
    }
    
    

    LoginController内容如下:

    package com.briup.security.web.controller;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.briup.security.util.JwtTokenUtils;
    import com.briup.security.util.Message;
    import com.briup.security.util.MessageUtil;
    
    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    
    /**
     * @description: 登陆
     * @author: wangzh
     * @create: 2019-03-21 15:56
     **/
    @Api(description = "登陆相关接口")
    @RestController
    @RequestMapping("/authentication")
    public class LoginController {
    
        @Autowired
        @Qualifier("userDetailServiceImpl")
        private UserDetailsService userDetailsService;
    
    
        @PostMapping("/form")
        @ApiOperation(value = "登入身份验证(JWT验证)", notes = "登入")
        public void login(String username, String password) {
            // TODO 这里面不需要写任何代码,由UserDeatilsService去处理
        }
    
        @GetMapping("/getUserDetailByToken")
        @ApiOperation(value = "根据token得到用户信息")
        public Message getUserDetailByToken(HttpServletRequest request, HttpServletResponse response) {
            String token = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
            response.setContentType("application/json;charset=UTF-8");
            if (token != null && StringUtils.startsWith(token, JwtTokenUtils.TOKEN_PREFIX)) {
                token = StringUtils.substring(token, JwtTokenUtils.TOKEN_PREFIX.length());
                UserDetails details = userDetailsService.loadUserByUsername(JwtTokenUtils.getUsername(token));
                return MessageUtil.success(details);
            } else {
                return MessageUtil.error(401, "token失效");
            }
        }
    
        
    
    }
    
    

    启动项目,访问swagger页面

    image.png

    点击用户接口,并进行访问结果如下:

    image.png

    因为我们在请求的时候并没有携带token,所以我们需要登陆,点击登陆相关接口,进行登陆

    image.png

    由此可以看见,当我们登陆成功时,会给我们返回一个token,token的值如下:

    Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlzcyI6Ikt1bnNoYW4gQnJpdXAiLCJleHAiOjE1NTM0MjU2ODcsImlhdCI6MTU1MzQyMjA4Nywicm9sIjpbeyJhdXRob3JpdHkiOiLnrqHnkIblkZgifV19.BTVjSR8ony9G-EdP8MOIDww0L2XoyyTPCpdA-quvLdjqT3evXVsPmHPfkq9mKmJieBYoQexBEbAf2E3Lf5SsgA
    

    如果登陆失败,则结果如下:

    image.png

    现在又会引发一个新的问题,我们通过前面的token校验器知道,发送请求的时候token会存放在请求头中,但是目前我们的swagger页面并没有让我们输入token的文本框,因此我们需要改造Swagger2Config,内容如下:

    package com.briup.security.config;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.ParameterBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.schema.ModelRef;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.Parameter;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    
    @Configuration
    @EnableSwagger2
    public class Swagger2Config {
        
        @Bean
        public Docket createRestApi() {
            ParameterBuilder tokenPar = new ParameterBuilder();
            List<Parameter> pars = new ArrayList<>();
            tokenPar.name("Authorization").description("令牌").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
            pars.add(tokenPar.build());
    
            return new Docket(DocumentationType.SWAGGER_2)
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.briup.security.web"))
                    .paths(PathSelectors.any())
                    .build()
                    .globalOperationParameters(pars)
                    .apiInfo(apiInfo());
    
        }
        
        
        
        
    /*  @Bean
        public Docket createRestApi() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.briup.security.web"))
                    .paths(PathSelectors.any())
                    .build();
        }*/
    
    
    
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("spring-security-demo")
                    .description("昆山杰普软件科技有限公司,http://www.briup.com")
                    .termsOfServiceUrl("http://www.briup.com")
                    .version("1.0")
                    .build();
        }
    }
    
    
    

    再次访问我们的swagger页面

    image.png

    将我们的令牌粘贴其中,并进行访问:

    image.png

    当我们把令牌输入错误是,结果如下:

    image.png

    至此,我们就已经完成了一个登陆验证。当然目前还是只是一个简单的表单验证,我会在后续的时间中,整理出手机短信验证,第三方登陆验证等等。

    相关文章

      网友评论

        本文标题:SpringBoot + Swagger + SpringSec

        本文链接:https://www.haomeiwen.com/subject/ezaytctx.html