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

简介

Spring Security

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

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

spring Security官网

JWT

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

工作流程

图片是网上找的。
在这里插入图片描述

实战应用

程序目录结构

在这里插入图片描述

引入依赖

        <!--java web token -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!--spring Security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

核心代码

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();
    }
}

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);
    }
}

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);
    }
}

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;
    }
}

自定义认证失败处理类

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());
        }
    }

}

获取用户权限信息

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

下面是源代码中注入角色时候自动添加ROLE_代码片段
在这里插入图片描述

@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();
    }
}

业务处理逻辑类

  • 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;
    }
}

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";
    }
}

测试接口

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

获取token信息

输出测试帐号 test和密码 123456,获取token
在这里插入图片描述

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

  • 访问http://localhost:8016/index 接口
  • 在请求头中添加token参数,把登陆返回的token参数放入到里面
  • 发送GET请求,可以看到结果中能访问
    在这里插入图片描述

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

  • 访问http://localhost:8016/admin 接口
  • 因为这个请求"hasRole(‘ADMIN’)" ,所以当前用户访问不了
    在这里插入图片描述

输入一个错误的token测试

在这里插入图片描述

项目配套代码

github地址
要是觉得我写的对你有点帮助的话,麻烦在github上帮我点 Star

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

猜你喜欢

转载自blog.csdn.net/ming19951224/article/details/107733689