【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证

文章目录

  • 1.简介
    • 1.1Spring Security
    • 1.2.JWT
  • 2.工作流程
  • 3.程序目录结构
  • 4.引入依赖
  • 5.核心代码
    • 5.1.Security配置类
    • 5.2.token生成
    • 5.3.JWT资源认证过滤器
    • 5.4.JwtUserDetail类
    • 5.5.自定义认证失败处理类
    • 5.6.获取用户权限信息
    • 5.7.业务处理逻辑类
    • 5.8.web接口
  • 6.测试接口
    • 6.1.获取token信息
    • 6.2.访问需要权限认证才能访问接口
    • 6.3.访问一个test帐号没有的角色资源
    • 6.4.输入一个错误的token测试
  • 7.项目配套代码

1.简介

1.1Spring Security

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。

Spring Security 相对于Shiro权限框架而已要稍微复杂一点.

spring Security官网

1.2.JWT

JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。
JWT官网

2.工作流程

【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第1张图片

3.程序目录结构

【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第2张图片

4.引入依赖

        
        
            io.jsonwebtoken
            jjwt
            0.9.1
        

        
        
            org.springframework.boot
            spring-boot-starter-security
        

5.核心代码

5.1.Security配置类

  • configureAuthentication() 配置身份验证信息
  • passwordEncoder() 配置密码的加密使用的算法
  • authenticationTokenFilterBean() 构建自定义jwt权限认证过滤器
  • configure() 配置资源过滤和注入自定义jwt过滤器
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

 	@Autowired
    private MyAuthExcetion.MyAuthenticationEntry myAuthenticationEntry;

    @Autowired
    private MyAuthExcetion.MyAccessDenied myAccessDenied;

    @Autowired
    private UserDetailsService userDetailsService;

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

    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置 UserDetailsService
                .userDetailsService(userDetailsService)
                // 使用 BCrypt 进行密码的 hash
                .passwordEncoder(passwordEncoder());
    }

    /**
     * 装载BCrypt密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置自定义jwt权限认证过滤器
     */
    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    /**
     * 设置需要授权认证的资源,不需要权认能访问的资源
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // we don't need CSRF because our token is invulnerable
                .csrf().disable()
                .cors().and() // 跨域
     		    .exceptionHandling().authenticationEntryPoint(myAuthenticationEntry).and()
                .exceptionHandling().accessDeniedHandler(myAccessDenied).and()
                // don't create session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                //过滤掉不需要权限的地址
                .antMatchers(HttpMethod.GET, "/").permitAll()
                .antMatchers("/sso/login").permitAll()
                // 其它接口都要认证
                .anyRequest().authenticated();

        //将token验证添加在密码验证前面
        httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);

        // disable page caching
        httpSecurity.headers().cacheControl();
    }
}

5.2.token生成

token参数配置
在application.yml中配置token参数信息

server:
  port: 8016
jwt:
  # 加密密钥
  secret: iwqjhda8232bjgh432[cicada-smile]
  #token有效时长 (单位秒)
  expire: 3600
  #请求头参数名称
  tokenHead: token

生成token工具类

@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtils {

    private String secret;
    private long expire;
    private String tokenHead;
	//..省略get set方法
	
    //jdk8新增的时钟类
    private Clock clock = DefaultClock.INSTANCE;

    /**
     * 使用用户名作为身份信息生成Token
     * @param identityId     用户身份标识
     * @param identityId     用户的角色权限信息
     */
    public Map getToken(String identityId, List<String> Authorizes) {
        Date nowDate = new Date();
        //token过期时间
        long expireAt=nowDate.getTime() + expire * 1000;
        Date expireDate = new Date(expireAt);
        Map map=new HashMap<>();
        map.put("expireAt",expireAt);
        String token= Jwts.builder()
                //放入唯一标识,可以是用户名或者Id
                .setSubject(identityId)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                //自定义属性 放入用户拥有角色和权限
                .claim("roleAuthorizes", Authorizes)
                .compact();
        map.put("token",token);
        return map;
    }

    /**
     * 根据token获取身份信息
     */
    public Claims getTokenClaim(String token) {
        try {
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 判断token是否失效
     */
    public boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(clock.now());
    }

    /**
     * 根据token获取username
     */
    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    /**
     * 根据token获取失效时间
     */
    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

    public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = getTokenClaim(token);
        return claimsResolver.apply(claims);
    }
}

5.3.JWT资源认证过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtils jwtTokenUtils;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String token = request.getHeader(jwtTokenUtils.getTokenHead());
        //判断当前请求中包含令牌
        if (!StringUtils.isEmpty(token)) {
            //重token中获取用户的角色权限信息
            Claims claims = jwtTokenUtils.getTokenClaim(token);
            if (claims != null) {
                //如果token未失效 并且 当前上下文权限凭证为null
                if (!jwtTokenUtils.isTokenExpired(token) && SecurityContextHolder.getContext().getAuthentication() == null) {
                    /**
                     * 这里省略了查询数据库token存不存在,有没有失效这个步骤
                     * 如果是做单点登录,后登录的人登录时候把上一个人的token状态改为失效,这样就能保证同一时间一个帐号只能有一个人能使用
                     */
                    JwtUserDetail userDetails = new JwtUserDetail(claims);
                    //把用户权限信息放到上下文中
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(request, response);
    }
}

5.4.JwtUserDetail类

/**
 * @author Dominick Li
 * @CreateTime 2020/4/16 21:28
 * @description 重token中获取用户权限信息
 **/
public class JwtUserDetail implements UserDetails {

    private String username;
    private Collection<SimpleGrantedAuthority> authorities;

    public JwtUserDetail(Claims claims) {
        this.username = claims.getSubject();
        //重token中获取权限信息
        List<String> roleAuthorizes = claims.get("roleAuthorizes", List.class);
        //权限和角色都在roleAuthorizes中,和shiro不同,shiro是权限和角色分开的
        authorities = new ArrayList<>(roleAuthorizes.size());
        roleAuthorizes.forEach((roleAuthorize) -> {
            authorities.add(new SimpleGrantedAuthority(roleAuthorize));
        });
    }

    @Override
    public Collection<SimpleGrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    @JsonIgnore
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return 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;
    }
}

5.5.自定义认证失败处理类

public class MyAuthExcetion {

    @Component
    public static class MyAuthenticationEntry implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            //token认证失败
            HashMap map = new HashMap();
            map.put("code", 402);
            map.put("msg", "toekn is invalid");
            response.getWriter().println(map.toString());
        }
    }

    @Component
    public static class MyAccessDenied implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
            //角色权限认证失败
            HashMap map = new HashMap();
            map.put("code", 401);
            map.put("msg", "accessDenied");
            response.getWriter().println(map.toString());
        }
    }

}

5.6.获取用户权限信息

为方便测试,用户角色权限信息写死了
需要注意的事项,在把角色放入到资源列表中需要在角色前面添加ROLE_

下面是源代码中注入角色时候自动添加ROLE_代码片段
【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第3张图片

@Service
public class UserDetailsFactory implements UserDetailsService {

    /**
     * 模拟数据库
     * 存放用户对应的角色
     */
    HashMap<String, String> userRole = new HashMap<>();

    {
        userRole.put("test", "USER");
        userRole.put("admin", "ADMIN");
    }

    /**
     * 模拟数据库
     * 存放角色对应的权限
     */
    HashMap<String, List<GrantedAuthority>> roleAuthorize = new HashMap<>();

    {
        List<GrantedAuthority> uList = new ArrayList<>();
        uList.add(new SimpleGrantedAuthority("select"));
        uList.add(new SimpleGrantedAuthority("put"));
        roleAuthorize.put("USER", uList);

        List<GrantedAuthority> aList =new ArrayList<>();
        aList.add(new SimpleGrantedAuthority("select"));
        aList.add(new SimpleGrantedAuthority("delete"));
        roleAuthorize.put("ADMIN", aList);
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //加密过的密码  123456
        String password="$2a$10$JA61ycV.otx/d8cJSPAi8Ozbvij5QwDTFxp7jq9Qq3pm0xuZbW6ji";
        String roleName=userRole.get(username);
        List<GrantedAuthority> authorityList=roleAuthorize.get(roleName);
        //security资源注解是把角色和权限一起处理的,shiro是分开处理,需要手动在角色前面添加ROLE_
        roleName="ROLE_"+roleName;
        authorityList.add(new SimpleGrantedAuthority(roleName));
        return User.builder().username(username).password(password).authorities(authorityList).build();
    }
}

5.7.业务处理逻辑类

  • passwordEncoder.encode() 对密码进行加密
  • passwordEncoder.matches() 用原始密码匹配和加密过的匹配进行匹配
@Component
public class UserService {

    @Autowired
    private JwtTokenUtils jwtTokenUtils;

    @Autowired
    UserDetailsFactory userDetailService;

    @Autowired
    PasswordEncoder passwordEncoder;

    public Map login(String username, String password) {
        Map map = null;
        UserDetails user = userDetailService.loadUserByUsername(username);
        if (!passwordEncoder.matches(/*原始密码*/password,/*加密过的密码*/user.getPassword())) {
            map = new HashMap();
            map.put("code", 303);
            map.put("msg", "密码错误");
        }
        List<String> authorites = new ArrayList<>(user.getAuthorities().size());
        for (GrantedAuthority authorite : user.getAuthorities()) {
            authorites.add(authorite.getAuthority());
        }
        map = jwtTokenUtils.getToken(username, authorites);
        map.put("code",200);
        return map;
    }
}

5.8.web接口

  • @PreAuthorize 资源认证注解
    hasRole() 角色拦截
    hasAuthority() 权限拦截
@RestController
public class TokenController {


    @Autowired
    private UserService userService;

    /**
     * 登录接口
     */
    @PostMapping("/sso/login")
    public Map login(@RequestParam("username") String username,
                        @RequestParam("password") String password) {
        // 省略数据源校验
        return userService.login(username,password);
    }

    /**
     * 需要角色user才能访问的地址
     */
    @PreAuthorize("hasRole('USER')")
    @GetMapping("/index")
    public String info() {
        return "index";
    }

    /**
     * 需要角色admin才能访问的地址
     */
    @GetMapping("/admin")
    @PreAuthorize("hasRole('ADMIN')")
    public String admin() {
        return "admin";
    }


    /**
     * 需要权限delete 才能删除用户信息
     */
    @DeleteMapping("/user/{id}")
    @PreAuthorize("hasAuthority('delete')")
    public String deleteUser(@PathVariable Integer id) {
        System.out.println("删除用户:id=" + id);
        return "user";
    }


    /**
     * 需要权限select 才能查看用户信息
     */
    @GetMapping("/user/{id}")
    @PreAuthorize("hasAuthority('select')")
    public String user(@PathVariable Integer id) {
        System.out.println("查询用户:id=" + id);
        return "user";
    }

    /**
     * 需要权限put 才能修改用户信息
     */
    @PutMapping("/user")
    @PreAuthorize("hasAuthority('put')")
    public String putUser() {
        System.out.println("修改用户:id");
        return "user";
    }
}

6.测试接口

  • test帐号角色为: USER
  • test帐号权限为: select,put

6.1.获取token信息

输出测试帐号 test和密码 123456,获取token
【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第4张图片

6.2.访问需要权限认证才能访问接口

  • 访问http://localhost:8016/index 接口
  • 在请求头中添加token参数,把登陆返回的token参数放入到里面
  • 发送GET请求,可以看到结果中能访问
    【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第5张图片

6.3.访问一个test帐号没有的角色资源

  • 访问http://localhost:8016/admin 接口
  • 因为这个请求"hasRole(‘ADMIN’)" ,所以当前用户访问不了
    【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第6张图片

6.4.输入一个错误的token测试

【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证_第7张图片

7.项目配套代码

github地址

创作不易,要是觉得我写的对你有点帮助的话,麻烦在github上帮我点下 Star

【SpringBoot框架篇】其它文章如下,后续会继续更新。

  • 1.搭建第一个springboot项目
  • 2.Thymeleaf模板引擎实战
  • 3.优化代码,让代码更简洁高效
  • 4.集成jta-atomikos实现分布式事务
  • 5.分布式锁的实现方式
  • 6.docker部署,并挂载配置文件到宿主机上面
  • 7.项目发布到生产环境
  • 8.搭建自己的spring-boot-starter
  • 9.dubbo入门实战
  • 10.API接口限流实战
  • 11.Spring Data Jpa实战
  • 12.使用druid的monitor工具查看sql执行性能
  • 13.使用springboot admin对springboot应用进行监控
  • 14.mybatis-plus实战
  • 15.使用shiro对web应用进行权限认证
  • 16.security整合jwt实现对前后端分离的项目进行权限认证
  • 17.使用swagger2生成RESTful风格的接口文档
  • 18.使用Netty加websocket实现在线聊天功能
  • 19.使用spring-session加redis来实现session共享
  • 20.自定义@Configuration配置类启用开关
  • 21.对springboot框架编译后的jar文件瘦身
  • 22.集成RocketMQ实现消息发布和订阅
  • 23.集成smart-doc插件零侵入自动生成RESTful格式API文档
  • 24.集成FastDFS实现文件的分布式存储

你可能感兴趣的:(springBoot,jwt,spring,boot)