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

x33g5p2x  于2022-02-16 转载在 Spring  
字(15.3k)|赞(0)|评价(0)|浏览(178)

这里有一个简单例子实现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接口数据:

相关文章

微信公众号

最新文章

更多