后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

Posted zhangphil

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后端架构token授权认证机制:spring security JSON Web Token(JWT)简例相关的知识,希望对你有一定的参考价值。

这里有一个简单例子实现token认证授权访问:

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例_Zhang Phil-CSDN博客

再写一个例子,该例中,假设

/api需要登录且具有有效token才能访问的接口。

/home是公开的接口,无须登录授权均可访问。

/login是登录接口,用户通过/login提交用户名和密码进行鉴权验证。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;
import java.util.Map;


@RestController
public class MyController 
    @Autowired
    private AuthenticationManager authenticationManager;

    //@Autowired
    //private MyUserDetailsService userDetailsService;

    @RequestMapping("/api")
    private String api(@RequestHeader Map<String, String> headers) 
        System.out.println("headers: " + headers);

        String token_header = headers.get("authorization");
        System.out.println("token: " + token_header);

        // token一般又称之为"Bearer token",以Bearer开头
        // 截取纯粹的token
        String BEARER = "Bearer ";
        String token = token_header.substring(BEARER.length());//从Bearer 之后开始截取

        String username = JwtTokenUtil.getUsername(token);
        String role = JwtTokenUtil.getUserRole(token);
        return username + " - " + role + " @api";
    

    /**
     * 这里的MyRequest封装了用户登录需要填写上报的信息。也可以通过把用户名和密码写入http中的header实现上报。
     *
     * @param request
     * @param response
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    private
    //ResponseEntity<?>
    void login(@RequestBody MyRequest request, HttpServletResponse response) 
        System.out.println("/login");

        String username = request.getUsername();
        String password = request.getPassword();
        System.out.println("用户名:" + username);
        System.out.println("密码:" + password);

        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, password);
        authenticationManager.authenticate(authentication);

        //UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        String token = JwtTokenUtil.createToken(username, "ADMIN");
        //return ResponseEntity.ok(token);

        // 设置编码
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        // 请求头里返回token
        // "Bearer "前缀token
        response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
        response.setContentType("text/json;charset=utf-8");

        try 
            response.getWriter().write("登录成功");
         catch (Exception e) 
            e.printStackTrace();
        
    

    @RequestMapping(value = "/home")
    private String home() 
        return "home";
    

token工具类:

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtTokenUtil 
    private static final String USER_NAME = "username";

    // Token头
    public static final String TOKEN_HEADER = "Authorization";

    // Token前缀
    public static final String TOKEN_PREFIX = "Bearer ";

    // 签名主题
    public static final String SUBJECT = "zhangphil";

    // 过期时间
    public static final long EXPIRITION = 3 * 60 * 1000;

    // 应用密钥
    public static final String APPSECRET_KEY = "zhangphil_secret";

    // 角色权限声明
    private static final String ROLE_CLAIMS = "role";

    /**
     * 生成Token
     */
    public static String createToken(String username, String role) 
        Map<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, role);

        String token = Jwts
                .builder()
                .setSubject(SUBJECT)
                .setClaims(map)
                .claim(USER_NAME, username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
                .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY)
                .compact();

        return token;
    

    /**
     * 校验Token
     */
    public static Claims checkJWT(String token) 
        Claims claims = null;
        try 
            claims = Jwts.parser()
                    .setSigningKey(APPSECRET_KEY)
                    .parseClaimsJws(token)
                    .getBody();

            return claims;
         catch (Exception e) 
            e.printStackTrace();
        

        return claims;
    

    /**
     * 从Token中提取username
     */
    public static String getUsername(String token) 
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get(USER_NAME).toString();
    

    /**
     * 从Token中获取用户角色
     */
    public static String getUserRole(String token) 
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("role").toString();
    

    /**
     * 校验Token是否过期
     */
    public static boolean isExpiration(String token) 
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.getExpiration().before(new Date());
    

token工具类中使用的类需要在pom.xml添加引用:

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

权限异常处理:

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

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

public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint 
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException 
        response.setCharacterEncoding("utf-8");
        response.setContentType("text/javascript;charset=utf-8");
        response.getWriter().print("没有访问权限");
    

用户登录鉴权过滤类:

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;

public class MyBasicAuthenticationFilter extends BasicAuthenticationFilter 
    public MyBasicAuthenticationFilter(AuthenticationManager authenticationManager) 
        super(authenticationManager);
    

    /**
     * 过滤http请求
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException 
        System.out.println(getClass().getSimpleName() + " doFilterInternal");

        String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);

        // 请求头中没Authorization或是Authorization不以Bearer开头
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) 
            chain.doFilter(request, response);
            System.out.println("token为空,或者token头部不是以Bearer 开头");
            return;
        

        // 若请求头中有token
        // 设置认证信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));

        super.doFilterInternal(request, response, chain);
    

    /**
     * 从token中获取用户信息并新建一个token。
     *
     * @param tokenHeader 字符串形式的Token请求头
     * @return 带用户名和密码以及权限的Authentication
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) 
        // 去掉前缀,获取Token字
        String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");

        // 从Token中获取用户名
        String username = JwtTokenUtil.getUsername(token);

        // 从Token中解密获取用户角色
        String role = JwtTokenUtil.getUserRole(token);

        // 将[ROLE_XXX,ROLE_YYY]格式角色字符串转换为数组
        String[] roles = StringUtils.strip(role, "[]").split(", ");

        Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();

        for (String s : roles) 
            authorities.add(new SimpleGrantedAuthority(s));
        

        if (username != null) 
            return new UsernamePasswordAuthenticationToken(username, null, authorities);
        

        return null;
    

用于用户登录密码编解码处理的类:

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;


/**
 * 注意,此处是明文,实际场景时候要加密
 */
@Component
public class MyPasswordEncoder extends BCryptPasswordEncoder 
    @Override
    public String encode(CharSequence rawPassword) 
        return rawPassword.toString();
    

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) 
        if (rawPassword == null) 
            throw new IllegalArgumentException("rawPassword为空!");
        

        if (encodedPassword == null || encodedPassword.length() == 0) 
            throw new IllegalArgumentException("encodedPassword为空");
        

        return encodedPassword.equals(rawPassword);
    

封装了用户(浏览器或客户端)发送请求的数据:

import java.io.Serializable;

public class MyRequest implements Serializable 
    private String username;
    private String password;

    //JSON Parsing
    public MyRequest() 

    

    public MyRequest(String username, String password) 
        this.setUsername(username);
        this.setPassword(password);
    

    public String getUsername() 
        return this.username;
    

    public void setUsername(String username) 
        this.username = username;
    

    public String getPassword() 
        return this.password;
    

    public void setPassword(String password) 
        this.password = password;
    

用户数据获取类,该类通常用于后端从后台的数据库根据用户名获取该用户的信息,供上层程序逻辑使用:

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.stereotype.Service;

import java.util.ArrayList;


@Service
public class MyUserDetailsService implements UserDetailsService 
    //假设已经知道用户名和密码,硬编码写死了用户名和密码
    //省去从数据库读取用户信息的过程
    public static final String USER_NAME = "zhangphil";
    public static final String USER_PASSWORD = "12345678";//正常情况密码应该加密,而不是明文。此处仅做演示。

    //数据库读写
    //@Autowired
    //private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        System.out.println(username + " 加载信息");

        if (USER_NAME.equals(username)) 
            //正常情况下,这里应该从数据库中,根据传入的用户名把用户信息读出来,然后返回User
            //从数据库中查用户
            //User user = userMapper.findUserByUsername(username);
            //简单演示期间,这里省略读数据库过程。
            return new User(USER_NAME, USER_PASSWORD, new ArrayList<>());
         else 
            throw new UsernameNotFoundException("用户不存在:" + username);
        
    

这里将为登录成功的用户返回正确的token:

import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;

/**
 * 验证用户名和密码
 * 验证通过后,生成token返回给客户端
 */

public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter 

    private AuthenticationManager authenticationManager;

    public MyUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) 
        this.authenticationManager = authenticationManager;
    

    /**
     * 验证
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 
        System.out.println("attemptAuthentication");

        // 从输入流中获取登录信息
        // 创建token调用authenticationManager.authenticate() 后,
        // 让Spring security框架内部验证
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                request.getParameter("username"),
                request.getParameter("password")));
    

    /**
     * 验证成功,生成token返回给客户端
     */
    @Override
    public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException 
        System.out.println("successfulAuthentication ...");

        User user = (User) authResult.getPrincipal();

        // 从User中获取权限信息
        Collection<? extends GrantedAuthority> authorities = user.getAuthorities();

        // 创建Token
        String token = JwtTokenUtil.createToken(user.getUsername(), authorities.toString());

        // 防乱码
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");

        // 在请求头返回token
        // 请求头带有"Bearer "前缀的token
        response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);

        // 防乱码
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write("登录成功");

        System.out.println("登录成功");
    

    /**
     * 验证失败
     */
    @Override
    public void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException 
        String retData;

        if (failed instanceof AccountExpiredException) 
            retData = "账号过期";
         else if (failed instanceof BadCredentialsException) 
            retData = "密码错误";
         else if (failed instanceof CredentialsExpiredException) 
            retData = "密码过期";
         else if (failed instanceof DisabledException) 
            retData = "账号不可用";
         else if (failed instanceof LockedException) 
            retData = "账号锁定";
         else if (failed instanceof InternalAuthenticationServiceException) 
            retData = "内部授权服务异常";
         else 
            retData = "未知异常";
        

        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(retData);
    

以上各个组件、工具类的“总装车间”,组合上面的组件,形成一台能够运行的token认证鉴权系统:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
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.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter 

    @Autowired
    private MyUserDetailsService userDetailsService;

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new MyPasswordEncoder());
    

    @Override
    public void configure(HttpSecurity http) throws Exception 
        http.csrf().disable()
                .authorizeRequests()
                //login为鉴权登录,排除授权拦截
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and()
                // 登录拦截
                .addFilter(new MyBasicAuthenticationFilter(authenticationManagerBean()))
                // JWT鉴权拦截
                .addFilter(new MyUsernamePasswordAuthenticationFilter(authenticationManagerBean()))
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling()
                // 用户访问无权限资源异常处理
                .authenticationEntryPoint(new MyAuthenticationEntryPoint());
    

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception 
        return super.authenticationManagerBean();
    

    /**
     * 假设这一部分接口是公开开放的,不需要token即可访问。
     * 这部分客户端http请求不拦截
     * 排除。
     */
    @Override
    public void configure(WebSecurity web) 
        web.ignoring()
                .antMatchers(
                        "/login**",
                        "/home**");
    

最后是一个很常规、简单的application启动加载器(入口Main“函数”):

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringTokenApplication 

    public static void main(String[] args) 
        SpringApplication.run(SpringTokenApplication.class, args);
    

启动SpringTokenApplication,/home无需鉴权认证可以直接访问:

/login提交用户名和密码:

登录成功后,后台系统返回token:

在/api接口的header里面填入token,获取接口数据:

后端系统正确返回/api接口数据:

开发者涨薪指南 48位大咖的思考法则、工作方式、逻辑体系

以上是关于后端架构token授权认证机制:spring security JSON Web Token(JWT)简例的主要内容,如果未能解决你的问题,请参考以下文章

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

后端架构token授权认证机制:spring security JSON Web Token(JWT)简例

秒懂SpringBoot之全网最易懂的Spring Security教程

spring boot整合jwt 实现前后端分离登录认证及授权

Spring Cloud微服务安全实战_5-2_基于session的SSO

最新版微服务架构鉴权解决方案Spring Cloud Gateway + Oauth2.0+mybatis+mysql+redis+nacos 统一认证和鉴权