欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

springboot + spring security + jwt实现api权限控制

程序员文章站 2022-05-05 23:13:50
...

1、在pom.xml中添加security和jwt的相关依赖,并在启动类上添加注解@EnableWebSecurity

<!-- 权限相关依赖(security和jwt)-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>2.0.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

2、新建用户实体类,继承了userDetails的属性,用于用户登录的授权验证

package com.rrf.securityjwt.dto;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * @Author:aha
 * @Description:
 * @Date: 2018/8/9 15:51
 * @Modified By:
 */
public class JwtUser implements UserDetails {

    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser() {
    }

    /**
     * 自定义值
     * @param phone
     * @param openId
     */
    public JwtUser(String phone,String openId) {
        username = phone;
        password = openId;
    }

    /**
     *
     * @return 获取权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    /**
     * 账号是否未过期,默认是false
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 账号是否未锁定,默认是false
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 账号凭证是否未过期,默认是false
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 默认也是false
     * @return
     */
    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public String toString() {
        return "JwtUser{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

3、在业务逻辑层重写UserDetailsService的loadUserByUsername方法,按实际需求来写相对应的“验证规则”即登录成功的评判或标准。

package com.rrf.securityjwt.service.impl;

import com.rrf.securityjwt.dto.JwtUser;
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;

/**
 * @Author:aha
 * @Description:自定义登录验证规则
 * @Date: 2018/8/9 16:00
 * @Modified By:
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    /**
     * 密码加密方式
     * @return
     */
    private BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    /**
     * 根据传进来的参数来确认凭证
     * 可以自定义 例如:用户的手机号->openid 可以对应于(name,password)
     * @param phone
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {

        /**
         * 根据参数获取凭证 可以对接数据库 redis等
         * 例如 当phone="15512343256"时,openId为"1002222224998989"
         * 验证时需要有密码规则即需要用户注册时将密码加密插入数据库
         * 举例:
         */
        String realPhone = "15512343256";
        String openId = "";
        if(realPhone.equals(phone)){
            openId = bCryptPasswordEncoder().encode("1002222224998989");
        }
        return new JwtUser(realPhone,openId);
    }
}

4、在控制器层写两个方法用于后面的测试

package com.rrf.securityjwt.controller;

import com.rrf.securityjwt.util.OkhttpUtil;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

/**
 * @Author:aha
 * @Description:
 * @Date: 2018/8/9 16:19
 * @Modified By:
 */
@RestController
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("/getNews")
    public String getNews() throws IOException {
        String news = OkhttpUtil.get("https://www.apiopen.top/journalismApi",null);
        return news;
    }

    @RequestMapping("/hello")
    public String hello() throws IOException {
        return "请求成功";
    }
}

5、新建JWTLoginFilter类,该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 attemptAuthentication 和successfulAuthentication ,当验证用户名密码正确后,生成一个token,并将token返回给客户端。

package com.rrf.securityjwt.filter;

import com.rrf.securityjwt.dto.JwtUser;
import com.rrf.securityjwt.util.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

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;

/**
 * 验证用户名密码正确后,生成一个token,并将token返回给客户端
 * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法
 * attemptAuthentication :接收并解析用户凭证。
 * successfulAuthentication :用户成功登录后,这个方法会被调用,我们在这个方法里生成token。
 * @author aha on 2018/8/6 17:47
 */
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTLoginFilter(AuthenticationManager authenticationManager){
        this.authenticationManager = authenticationManager;
    }

    /**
     *接收并解析用户凭证
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        //从输入流中获取登录信息
        try{
            System.out.println("尝试登录");

            //手机号
            String phone = request.getParameter("phone");

            //用户openId
            String openId = request.getParameter("openId");

            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            phone,openId,new ArrayList<>()
                    )
            );
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
     * 用户成功登录后,这个方法会被调用,我们在这个方法里生成token
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        String principal = ((JwtUser)authResult.getPrincipal()).getUsername();

        String token = JwtTokenUtils.createToken(principal,false);

        System.out.println("【登录成功,token->】"+JwtTokenUtils.TOKEN_PREFIX+token);
        response.addHeader(JwtTokenUtils.TOKEN_HEADER,JwtTokenUtils.TOKEN_PREFIX+token);
    }

    /**
     * 这是验证失败时候调用的方法
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }
}

6、新建JWTAuthenticationFilter类,该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。 如果校验通过,就认为这是一个取得授权的合法请求。

package com.rrf.securityjwt.filter;

import com.rrf.securityjwt.util.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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;

/**
 * @Author:aha
 * @Description:
 * token的校验
 * 该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,
 * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 * @Date: 2018/8/9 16:26
 * @Modified By:
 */
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager){
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader(JwtTokenUtils.TOKEN_HEADER);

        // 如果请求头中没有Authorization信息则直接放行了
        if(header == null || !header.startsWith(JwtTokenUtils.TOKEN_PREFIX)){
            chain.doFilter(request,response);
            return;
        }

        // 如果请求头中有token,则进行解析,并且设置认证信息
        UsernamePasswordAuthenticationToken authenticationToken = getAuthentication(header);

        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        chain.doFilter(request,response);
    }

    /**
     *这里从token中获取用户信息并新建一个token
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String header) {

        String token = header.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String principal = JwtTokenUtils.getUsername(token);

        if (principal != null) {
            return new UsernamePasswordAuthenticationToken(principal, null, new ArrayList<>());
        }
        return null;
    }
}

7、新建WebSecurityConfig类配置springsecurity,通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起。这里配置了,除hello/hello的get方法的请求不需要权限验证以外其余都需要验证才可以访问。

package com.rrf.securityjwt.security;

import com.rrf.securityjwt.filter.JWTAuthenticationFilter;
import com.rrf.securityjwt.filter.JWTLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * @Author:aha
 * @Description:
 * SpringSecurity的配置
 * 通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起
 * @Date: 2018/8/9 16:43
 * @Modified By:
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.GET,"/hello/hello").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()
                .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()));
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

jwttokenutil工具类

package com.rrf.securityjwt.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

/**
 * @Author:aha
 * @Description:jwt工具类
 * @Date: 2018/8/9 16:30
 * @Modified By:
 */
public class JwtTokenUtils {

    public static final String TOKEN_HEADER = "Authorization";

    public static final String TOKEN_PREFIX = "Bearer ";

    private static final String SECRET = "jwtsecretdemo";

    /**
     * 签发者
     */
    private static final String ISS = "LLy";

    /**
     * 过期时间是3600秒,既是1个小时
     */
    private static final long EXPIRATION = 3600L;

    /**
     * 选择了记住我之后的过期时间为7天
     */
    private static final long EXPIRATION_REMEMBER = 604800L;

    /**
     * 创建token
     * @param username
     * @param isRememberMe
     * @return
     */
    public static String createToken(String username, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        String token = Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
        return token;
    }

    /**
     * 从token中获取用户名
     * @param token
     * @return
     */
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }

    /**
     * 判断是否已过期
     * @param token
     * @return
     */
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }

    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

接下来用postman请求测试一下:

(1)当没有带token时hello/hello也是可以访问的

springboot + spring security + jwt实现api权限控制

 当没有带token时,访问hello/getNews,被拦截到登录页面

springboot + spring security + jwt实现api权限控制

此时访问登录接口即/login,这是security自己默认的登录接口(**这里是post请求方式),如果需要自定义登录页面的话可以在WebSecurityConfig配置类中修改  .formLogin() .loginPage(url)

当验证成功后,返回的请求头中会带Authorization,对应的值就是认证过后的jwttoken,固定格式是以Bearer 开头的

springboot + spring security + jwt实现api权限控制

接下来我们带上token访问需要认证的hello/getNews接口

访问成功!!!

springboot + spring security + jwt实现api权限控制

最后的最后,一切要以实际需求来自定义登录鉴权的标准。。。

项目地址:https://gitee.com/lanran1/security-jwt