Spring Security 初探 & 自定义身份认证

一直以来写Web应用时对于身份的验证和授权都比较简单,借助session attribute,又或者使用自己实现的认证拦截器(filter、interceptor……)等。

但其实spring security有提供一整套根据不同标准制定的处理认证授权的方法,而且还支持自定义认证。这次要讲的是通过自定义认证来实现JWT,关于spring security的更详细讲解还需要准备一下待下篇。

# - DEMO 目录结构

目录如下


  • src/main/java
    • indi.a9043.demo
      • config
        • SecurityConfig.java
      • controller
        • TestController.java
      • entity
        • TestUser.java
      • security
        • TestAccessDeniedHandler.java
        • TestAuthenticationEntryPoint.java
        • TestAuthenticationFailureHandler.java
        • TestAuthenticationFilter.java
        • TestAuthenticationSuccessHandler.java
      • service
        • TestUserDetailsService.java
      • util
        • JwtUtil.java
      • DemoApplication.java

如上表,

  • config 为项目配置类,demo只有一个security配置
  • controller 测试的接口
  • entity 认证用的实体类
  • security 自定义认证相关类
  • service 业务类,demo包含身份验证的Service
  • util 工具类,demo包含JWT使用工具类
  • DemoApplication main函数入口

# - 开始构建

本项目使用spring boot initializer预先添加,项目依赖和入口类,pom.xml 依赖如下,可以自行添加。

<!-- pom.xml -->

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <dependency>
        <groupId>org.json</groupId>
        <artifactId>json</artifactId>
        <version>20180130</version>
    </dependency>
</dependencies>

# - 新建用户实体以及用户操作类

  • 在entity包下创建TestUser 类并实现UserDetails接口

    package indi.a9043.demo.entity;
      
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    
    import java.util.Collection;
    
    /**
     * 仅考虑简单认证
     */
    public class TestUser implements UserDetails {
        private String userName;
        private String userPassword;
    
          @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return null;
            }
    
            @Override
            public String getPassword() {
                return this.userPassword;
            }
    
            @Override
            public String getUsername() {
                return this.userName;
            }
    
            @Override
            public boolean isAccountNonExpired() {
                return true;
            }
    
            @Override
            public boolean isAccountNonLocked() {
                return true;
            }
    
            @Override
            public boolean isCredentialsNonExpired() {
                return true;
            }
    
            @Override
            public boolean isEnabled() {
                return true;
            }
    
            //getter & setter
            public String getUserName() {
                return userName;
            }
        
            public TestUser setUserName(String userName) {
                this.userName = userName;
                return this;
            }
    
            public String getUserPassword() {
                return userPassword;
            }
    
            public TestUser setUserPassword(String userPassword) {
                this.userPassword = userPassword;
                return this;
            }
        }
    
  • 在service包下新建TestUserDetailsService 并实现UserDetailsService

    
    package indi.a9043.demo.service;
    
    import indi.a9043.demo.entity.TestUser;
    import org.springframework.beans.factory.annotation.Autowired;
    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.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    @Service
    public class TestUserDetailsService implements UserDetailsService {
        /*
         * 定义一个用户用来测试
         * 用户名: test1
         * 密码: 123456
         */
        private TestUser testUser;
    
        public TestUserDetailsService() {
            testUser = new TestUser();
            testUser.setUserName("test1");
            testUser.setUserPassword(new BCryptPasswordEncoder().encode("123456"));
        }
    
        /**
         * 根据用户名查询UserDetails
         *
         * @param username 用户名
         * @return UserDetails
         * @throws UsernameNotFoundException 无此用户名
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            if (username.equals(testUser.getUserName())) {
                return testUser;
            }
            throw new UsernameNotFoundException("User " + username + " was not found");
        }
    }
    

# - 新建JwtUtil 工具类

util包下

package indi.a9043.demo.util;

import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Optional;

/**
 * Token Util
 *
 * @author a9043
 */
public class JwtUtil {
    private static SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512;
    private static String stringKey = "a9043_xxx";
    private static byte[] encodedKey = Base64.encodeBase64(stringKey.getBytes());
    private static SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

    /**
     * 生成Token
     *
     * @param claims 声明
     * @return token
     */
    public static String createJWT(Map<String, Object> claims) {
        return createJWT(claims, Calendar.HOUR, 2);
    }

    /**
     * 自定义过期时间Token
     *
     * @param claims       声明
     * @param expireField  时间位
     * @param expireAmount 时间长
     * @return token
     */
    public static String createJWT(Map<String, Object> claims, int expireField, int expireAmount) {
        Date now = Calendar.getInstance().getTime();
        Calendar expireCal = Calendar.getInstance();
        expireCal.add(expireField, expireAmount);
        Date expire = expireCal.getTime();

        JwtBuilder builder = Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setHeaderParam("alg", "HS512")
                .setIssuedAt(now)
                .setClaims(claims)
                .setIssuer("a9043")
                .setExpiration(expire)
                .signWith(signatureAlgorithm, key);
        return builder.compact();
    }

    /**
     * Token parser
     *
     * @param JwtStr token
     * @return claims
     * @throws SignatureException  err
     * @throws ExpiredJwtException err
     */
    public static Claims parseJwt(String JwtStr) throws SignatureException, ExpiredJwtException {
        Claims claims = Jwts.parser()
                .setSigningKey(key)
                .parseClaimsJws(JwtStr)
                .getBody();
        return Optional
                .ofNullable(claims)
                .filter(claim -> claim.getExpiration() == null || !new Date().after(claim.getExpiration()))
                .orElse(null);
    }
}

# - 新建测试接口

controller 包下

@RestController
public class TestController {
    /**
     * 不受保护资源
     *
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    /**
     * 受保护资源
     *
     * @return
     */
    @GetMapping("/resource")
    public String resource() {
        return "resource";
    }
}

# - 新建token过滤器

security包下新建 TestAuthenticationFilter 继承 OncePerRequestFilter

package indi.a9043.demo.security;

import indi.a9043.demo.service.TestUserDetailsService;
import indi.a9043.demo.util.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class TestAuthenticationFilter extends OncePerRequestFilter {
    @Resource
    private TestUserDetailsService testUserDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        //获得header token
        String header = request.getHeader("Authorization");

        if (header == null || !header.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        String token = header.substring(7);
        Claims claims;

        if (token.length() <= 0 ||
                SecurityContextHolder.getContext().getAuthentication() != null) {
            return;
        }

        //解析token
        try {
            claims = JwtUtil.parseJwt(token);
            String userName = (String) claims.get("userName");
            UserDetails userDetails = testUserDetailsService.loadUserByUsername(userName);
            if (!userDetails.getUsername().equals(userName)) {
                SecurityContextHolder.clearContext();
                filterChain.doFilter(request, response);
                return;
            }
            //设定Authentication
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(
                            userDetails.getUsername(),
                            userDetails.getPassword(),
                            userDetails.getAuthorities());
            authentication.setDetails(
                    new WebAuthenticationDetailsSource().
                            buildDetails(
                                    request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch (MalformedJwtException | SignatureException | ExpiredJwtException e) {
            SecurityContextHolder.clearContext();
        }

        filterChain.doFilter(request, response);
    }
}

# - 新建各处理类

都新建在security包下

  • TestAuthenticationSuccessHandler 登录成功

    package indi.a9043.demo.security;
    
    import indi.a9043.demo.entity.TestUser;
    import indi.a9043.demo.util.JwtUtil;
    import org.json.JSONObject;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class TestAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request,
                                            HttpServletResponse response,
                                            Authentication authentication) throws IOException, ServletException {
            TestUser testUser = (TestUser) authentication.getPrincipal();
            Map<String, Object> claimsMap = new HashMap<>();
            claimsMap.put("userName", testUser.getUserName());
    
            String token = JwtUtil.createJWT(claimsMap);
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("access-token", token);
            response.getWriter().write(jsonObject.toString());
        }
    }
    
    
  • TestAuthenticationFailureHandler 登录失败

    package indi.a9043.demo.security;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    public class TestAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
            response.getWriter().write("failure");
        }
    }
    
  • TestAccessDeniedHandler 登录后的拒绝handler

    package indi.a9043.demo.security;
    
    import org.springframework.security.access.AccessDeniedException;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    public class TestAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            response.getWriter().write("denied");
        }
    }
    
  • TestAuthenticationEntryPoint 未认证入口

    package indi.a9043.demo.security;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    @Component
    public class TestAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
        }
    }
    

# - 配置 SecurityConfig

此为web security 配置类,不同版本和构建的spring可能需要带上 @EnableWebSecurity 注解

package indi.a9043.demo.config;

import indi.a9043.demo.security.*;
import indi.a9043.demo.service.TestUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.annotation.Resource;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private TestUserDetailsService testUserDetailsService;
    @Resource
    private TestAuthenticationEntryPoint testAuthenticationEntryPoint;
    @Resource
    private TestAuthenticationSuccessHandler testAuthenticationSuccessHandler;
    @Resource
    private TestAuthenticationFailureHandler testAuthenticationFailureHandler;
    @Resource
    private TestAccessDeniedHandler testAccessDeniedHandler;
    @Resource
    private DaoAuthenticationProvider daoAuthenticationProvider;
    @Resource
    private TestAuthenticationFilter testAuthenticationFilter;

    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
        /*
         * 自定义Provider
         * 自定义UserDetailsService
         * 自定义PasswordEncoder
         */
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(testUserDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        return daoAuthenticationProvider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /*
         * add 自定义provider
         */
        auth
                .authenticationProvider(daoAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // 关闭csrf保护
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 设置为无状态
                .and()
                .authorizeRequests()
                .antMatchers("/hello").permitAll() // 不受保护资源
                .anyRequest().authenticated() //受保护资源
                .and()
                .formLogin() //表单登录
                .loginProcessingUrl("/login") //表单登录api 不同于loginPage登录页面
                .successHandler(testAuthenticationSuccessHandler) //登录成功handler
                .failureHandler(testAuthenticationFailureHandler) //登录失败handler
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(testAuthenticationEntryPoint) //未授权入口
                .accessDeniedHandler(testAccessDeniedHandler) //拒绝入口
                .and()
                .addFilterBefore(testAuthenticationFilter,
                        UsernamePasswordAuthenticationFilter.class); //添加token过滤, 处理带token请求
    }
}

# - 测试

综上,已经security 已经配置好了,现在开始测试 rest api

  • 访问不受保护资源 /hello
    • req
    GET http://localhost:8080/hello
    Accept: */*
    Cache-Control: no-cache
    
    ###
    

    - res

hello


  

- 访问受保护资源 /resource

  - req

```http
GET http://localhost:8080/resource
Accept: */*
Cache-Control: no-cache

###
- res
{
  "timestamp": "2018-07-22T13:53:31.134+0000",
  "status": 401,
  "error": "Unauthorized",
  "message": "Full authentication is required to access this resource",
  "path": "/resource"
}
  • 登录接口 /login

    • req
    POST http://localhost:8080/login?username=test1&password=123456
    Accept: */*
    Cache-Control: no-cache
    
    ###
    
    • res
    {
      "access-token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhOTA0MyIsInVzZXJOYW1lIjoidGVzdDEiLCJleHAiOjE1MzIyNzQ0MjR9.sKfslwlYyn8lKZ0UQdNqKdBB2aheoufs5O2p3brUxxKyn5cAN_0hOqCB7LkmhdMVe4XIwpjAiez48Gqwqb8AIQ"
    }
    
  • 访问受保护资源 /resource

    • req
    GET http://localhost:8080/resource
    Accept: */*
    Cache-Control: no-cache
    Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJhOTA0MyIsInVzZXJOYW1lIjoidGVzdDEiLCJleHAiOjE1MzIyNzQ0MjR9.sKfslwlYyn8lKZ0UQdNqKdBB2aheoufs5O2p3brUxxKyn5cAN_0hOqCB7LkmhdMVe4XIwpjAiez48Gqwqb8AIQ
    
    ###
    
    • res
    resource
    

# - 总结

没有总结。。自此,我总算尝试了通过Spring Security,使用自定义方式进行身份认证。


***
demo 源代码托管在github: //github.com/190434957/spring_security_JWT_DEMO> ",