Springboot+Spring-Security+JWT实现restful Api的权限管理(适合大部分需要权限管理的系统)

全局目录

    • 前言
    • JWT
    • Spring Security
    • 简单说下实现的思路
    • 实现

前言

这学期工程实践的时候接触到权限管理,当时只是了解到这样的一个名词,暑假实习的时候,刚好要做一个项目就需要权限管理。就看很多文章简单总结了权限管理的一个初步解决方案。个人认为权限管理很很难的内容,自己总结的这个只能算是入门。更多的需要深入的学习。
在实际操作之前先简单介绍几个概念吧

JWT

1.什么是JWT
JWT(json web token),是一种用于web传输的json形式的用户身份令牌(token)。在一个session不共享的web环境中,用户信息可以通过JWT来传递,每一次请求都携带用户身份信息。
2.JWT组成
JWT的组成分为3个部分,
1是头部信息(Header),
2是有效荷载(Payload),
3是签名(Signature)。
1.header,JWT的头部分为两部分:声明类型,声明加密的算法,通常是HMACSHA256.然后把头部进行base64加密
2.playload,存放信息的地方。标准中注册的声明,公共的声明,私有的声明;每一部分都有各自的信息。然后接着进行base64加密
3.signature:是一个签证信息,把header和payload加密后的信息加上加盐的secret组合加密

https://jwt.io/ 该网站可以生成JWT。(实际项目中需要自定义生成和解析token)

Spring Security

1.定义:
Spring Security是一种基于Spring AOP和Servlet规范中的Filter实现的安全框架。它能够在Web请求级别和方法调用级别处理身份认证和授权。 就是前端请求在请求后端接口之间的过程中处理逻辑。
**2. spring security 内置拦截器顺序

验证拦截器:UsernamePasswordAuthenticationFilter,
授权拦截器:BasicAuthenticationFilte
springSecurity配置: WebSecurityConfigurerAdapter
每次请求只有经过上面的3个拦截器后才能到达后端接口

当然Spring security的拦截器不止这些,这3个只是冰山一角,但一般这3个就够用了。也暂时只接触到了这些。而且也只是很浅的理解。

简单说下实现的思路

  1. 用户输入登陆信息点击登陆,首先是UsernamePasswordAuthenticationFilter这个拦截器将用户信息拦截下来,框架根据用户信息查询数据库信息来验证用户,如果验证成功,就执行UsernamePasswordAuthenticationFilter这个接口下的successfulAuthentication方法,在这个方法里你可以写你的逻辑处理。比如返回一个token。失败后执行UsernamePasswordAuthenticationFilter这个接口下的unsuccessfulAuthentication,同样这个方法里也可以基于http请求返回前端一些信息。
  2. 前端接受到这个token后每次请求都将携带这个token在请求头的Authorization中,前面登陆成功前端跳转了页面,当点击一个需要用户权限的接口的时候在请求接口的同时将token一起给后台。BasicAuthenticationFilter这个拦截器就会拦截到你的请求,doFilterInternal这个方法里面你可以自定义验证。授权。
  3. 最后进入最后一个拦截器WebSecurityConfigurerAdapter,在这里你可以设置那些接口需要那些用户角色才可以放问。通过设置后端接口的访问权限来实现前台页面的权限管理。
  4. 经过了上面的3次拦截就可以进入后端的controller访问接口了。

实现

第一步:载入相关的依赖

这里只导入了权限管理的想关依赖

		<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        <dependency>
            <groupId>io.jsonwebtokengroupId>
            <artifactId>jjwtartifactId>
            <version>0.9.1version>
        dependency>

第二部:实体类

package com.report.system.assist.pojo;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

@Data
@TableName("tb_employee")//指定特定的数据库表,但只要不是差别很大,不用指定也可以
public class Employee {
    //主键
    @TableId("id")//用来标识实体类的主键,以便插件在生成主键雪花Id的时候找到哪个是主键。
    private String id;//因为指定了id所所以这名字可以任取,不与数据库id名一样
    private String mobile;
    private String username;
    private String password;
    @TableField("role")//当取名不一样可以用这个注解
    private String role;
    private String role2;
    private String role3;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date regtime;
}

第三步:JwtTokenUtils,这主要是封装了处理token的方法

package com.report.system.assist.common.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;

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 = "echisan";
  // 过期时间是3600秒,既是1个小时
   private static final long EXPIRATION = 3600L;
    // 选择了记住我之后的过期时间为7天
    private static final long EXPIRATION_REMEMBER = 604800L;
    private static final String ROLE_CLAIMS ="ROLE_" ;
    /**
     * 生成JWT
     * @param
     * @param username
     * @return
     */
    public static String createToken(String username,String role) {
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        HashMap<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, role);//使用payload去存储我们的用户角色信息
        JwtBuilder builder = Jwts.builder()//更具id,subject,key创建Token
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .setClaims(map)
                .setSubject(username)
                .setIssuedAt(now);
        if (EXPIRATION > 0) {
            builder.setExpiration(new Date(nowMillis + EXPIRATION * 1000));//一小时后过期
        }
            return builder.compact();
    }
    /**
     * 解析JWT
     * @param token
     * @return
     */
    public static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
    //从token中获取用户名
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }
    // 是否已过期
    public static boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }
    //从token中获取角色
    public static String getUserRole(String token) {
        System.out.println("从token中获取角色"+getTokenBody(token).get(ROLE_CLAIMS));
        return (String) getTokenBody(token).get(ROLE_CLAIMS);
    }
}

第四步:EmployeeMapper
写一个根据用户名查询用户返回用户实体的方式,框架会用到。

public interface EmployeeMapper extends BaseMapper<Employee> {
}

我用的是mybatis-plus,只是继承了这个通用mapper,具体查询写在了调用处,如果你是jpa需要

public interface UserRepository extends CrudRepository<User, Integer> {
    User findByUsername(String username);
}

第五步:UserDetailsService
使用springSecurity需要实现UserDetailsService接口供权限框架调用,(这里需要理解一下,就是你只管实现这个接口然后调用这个查询方法,其他就什么都不管了,框架需要这个方法来做验证就是第一个拦截器需要用到)该方法只需要实现一个方法就可以了,那就是根据用户名去获取用户,那就是上面EmployeeMapper了,这里直接调用了。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        QueryWrapper<Employee> queryWrapper = new QueryWrapper<Employee>();
        queryWrapper.eq("mobile",s);
        Employee employee = employeeMapper.selectOne(queryWrapper);
        return new JwtUser(employee);
    }
}

由于接口方法需要返回一个UserDetails类型的接口,所以这边就再写一个类去实现一下这个接口。这个类和数据库无关,只是暂时处理逻辑的。
第六步:JwtUser

@Data
public class JwtUser implements UserDetails {

    private String id;
    private String username;
    private String password;
    private String role;
    private Collection<? extends GrantedAuthority> authorities;
    public JwtUser() {
    }
    // 写一个能直接使用user创建jwtUser的构造器
    public JwtUser(Employee employee) {
        id = employee.getId();
        username = employee.getUsername();
        password = employee.getPassword();
        //这里只存贮了一个角色名
        authorities = Collections.singleton(new SimpleGrantedAuthority(employee.getRole()));
    }
    // 获取权限信息,目前只会拿来存角色。。getAuthorities获取用户权限,springSecurity用来获取用户权限的方法
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    // 账号是否未过期,默认是false,记得要改一下
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 账号是否未锁定,默认是false,记得也要改一下
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 账号凭证是否未过期,默认是false,记得还要改一下
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    //这个有点抽象不会翻译,默认也是false,记得改一下
    @Override
    public boolean isEnabled() {
        return true;
    }
}

第七步:接下来就需要配置拦截器啦
JWTAuthenticationFilter:鉴权
JWTAuthenticationFilter继承于UsernamePasswordAuthenticationFilter
该拦截器用于获取用户登录的信息,只需创建一个token并调用authenticationManager.authenticate()让spring-security去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作。
验证的事情交给框架。
献上这一部分的代码。(都有注释)

package com.report.system.assist.config;
import com.report.system.assist.common.util.JwtTokenUtils;
import com.report.system.assist.pojo.JwtUser;
import com.report.system.assist.pojo.LoginUser;
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.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilte;
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 JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {//UsernamePasswordAuthenticationFilter拦截登陆请求
    private AuthenticationManager authenticationManager;
//登陆页面
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/user/login");
    }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {
        // 从输入流中获取到登录的信息,通过输入的信息框架去数据库中查找是否匹配,然后成功或者失败,结束
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "*");
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_OK);
            return null;
        }
//        response.setHeader("Access-Control-Allow-Headers","Authorization");
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        LoginUser loginUser = new LoginUser();
        loginUser.setUsername(username);
        loginUser.setPassword(password);
        //LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
        //创建一个UsernamePasswordAuthenticationToken该token包含用户的角色信息,而不是一个空的ArrayList,查看一下源代码是有以下一个构造方法的。
      //调用authenticationManager.authenticate()让spring-security去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作
        return authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
        );
    }
   // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        // 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
        // 所以就是JwtUser啦
        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("这里是第一个拦截");
        String role = "";
        // 因为在JwtUser中存了权限信息,可以直接获取,由于只有一个角色就这么干了
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        for (GrantedAuthority authority : authorities){
            System.out.println("鉴权"+authority.getAuthority());
            role = authority.getAuthority();
        }
        // 根据用户名,角色创建token
        String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role);
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的格式应该是 `Bearer token`
        response.setHeader("Access-Control-Expose-Headers", "token");
        response.setHeader("token", token);//JwtTokenUtils.TOKEN_PREFIX +
        System.out.println("成功返回token");
    }
    // 这是验证失败时候调用的方法
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
       response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }
}

第八步:JWTAuthorizationFilter:授权
验证成功当然就是进行鉴权了,每一次需要权限的请求都需要检查该用户是否有该权限去操作该资源,当然这也是框架帮我们做的,那么我们需要做什么呢?很简单,只要告诉spring-security该用户是否已登录,是什么角色,拥有什么权限就可以了。
JWTAuthenticationFilter继承于BasicAuthenticationFilter,确保过滤器的顺序,JWTAuthorizationFilter在JWTAuthenticationFilter后面就没问题了。

package com.report.system.assist.config;

import com.report.system.assist.common.util.JwtTokenUtils;
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.Collections;
//验证用户权限的拦截器
//只要告诉spring-security该用户是否已登录,是什么角色,拥有什么权限就可以了
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "*");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "*");
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_OK);
            return;
        }
        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        System.out.println("授权1:"+tokenHeader);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }
    // 这里从token中获取用户信息并新建一个token
    //解析token,检查是否能从token中取出username,如果有就算成功了\
    //再根据该username创建一个UsernamePasswordAuthenticationToken对象
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        String username = JwtTokenUtils.getUsername(token);
        String role = JwtTokenUtils.getUserRole(token);
        System.out.println("ssd"+role);
        if (username != null){
            //假如能从token中获取用户名就该token验证成功
            //创建一个UsernamePasswordAuthenticationToken该token包含用户的角色信息,而不是一个空的ArrayList,查看一下源代码是有以下一个构造方法的。
            return new UsernamePasswordAuthenticationToken(username, role,
                    Collections.singleton(new SimpleGrantedAuthority(role))
            );
        }
        return null;
    }
}

第九步:配置SpringSecurity
这里基本操作都写好啦,现在就需要我们将这些辛苦写好的“组件”组合到一起发挥作用了,那就需要配置了。需要开启一下注解@EnableWebSecurity然后再继承一下WebSecurityConfigurerAdapter就可以啦,springboot就是可以为所欲为~

package com.report.system.assist.config;
import com.report.system.assist.service.UserDetailsServiceImpl;
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.http.HttpMethod;
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.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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
 * 安全配置类,替换默认的,只用里面的加盐算法
 *///springSecurity配置
@Configuration//让系统知道我是一个配置类
@EnableWebSecurity//
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {//
    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsServiceImpl userDetailsService;
    /**
     * 要用Security的密码加盐算法必须要写这部分
     * authorize:授权
     * authenticated:认证
     * authorizeRequests所有security全注解配置实现的开端,表示说明开始需要的权限
     * 需要的权限分两部分:第一部分是拦截的路径,第二部分是访问该路径需要的权限
     * antMatchers:拦截路径"/**所有路径",permitAll():任何权限都可以直接通行
     * anyRequest:任何的请求,authenticated认证后才可以访问
     * .and().csrf().disable();固定写法,表示使csrf(一种网络攻击技术)拦截失效
     *  // 测试用资源,需要验证了的用户才能访问
     *                 // 其他都放行了
     *                 // .anyRequest().permitAll()
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http   .csrf().disable()
                .authorizeRequests()        //定义那些url需要保护,那些不需要保护。.permitAll()就是不需要保护
               // .antMatchers("/tasks/**").authenticated() // 测试用资源,需要验证了的用户才能访问,加了这里后面不作用
                .antMatchers(HttpMethod.POST, "/tasks/**").hasAuthority("ADMIN")//需要角色为ADMIN才能删除该资源
                .antMatchers(HttpMethod.DELETE, "/tasks/**").hasAuthority("ADMIN")
                .antMatchers("/auth/**").permitAll()  //在设置的路径下的可以直接通过。
                .anyRequest().permitAll()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                .addFilter(new CorsFilter(corsConfigurationSource()))
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    }
    @Configuration
    public class CorsConf {
        @Bean
        public CorsFilter corsFilter() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.addAllowedOrigin("*");
            corsConfiguration.addAllowedHeader("Content-Type,Authorization");
            corsConfiguration.addAllowedMethod("*");
            source.registerCorsConfiguration("/**", corsConfiguration);
            return new CorsFilter(source);
        }
    }
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
    // 加密密码的,安全第一嘛~
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

第十步:就是写你的controller了,写一个注册的吧

 @PostMapping("/register/{code}")
    public Result register(@PathVariable String code, Employee employee){
        System.out.println("注册");
        employeeService.register(code,employee);
        return new Result(true, StatusCode.OK, "注册成功,请登录!");
    }

登陆
我们看一下UsernamePasswordAuthenticationFilter的源代码

	public UsernamePasswordAuthenticationFilter() {
		super(new AntPathRequestMatcher("/login", "POST"));
	}

可以看出来默认是/login,所以登录直接使用这个路径就可以啦~当然也可以自定义
只需要在JWTAuthenticationFilter的构造方法中加入下面那一句话就可以啦

 public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/auth/login");
    }

ok接下来就需要你去测试了。
在配置SpringSecurity的时候说说明了那些用户权限可以访问那些接口,

你可能感兴趣的:(SpringBoot)