Springboot架构下集成了JWT和spring security的权限、安全校验的rest API

JWT:

JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。
由于数据是使用数字签名的,所以是可信任的和安全的。
JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。

JWT的工作流程

  • 用户导航到登录页,输入用户名、密码,进行登录
  • 服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
  • 服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
  • 用户得到token,存在localStorage、cookie或其它数据存储形式中。
  • 以后用户请求/protected中的API时,在请求的header中加入 Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
  • 服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
  • 用户取得结果

JWT的工作流程图

JWT的工作流程图

JWT的数据格式组成

JWT是由三段组成的,按官方的叫法分别是header(头)、payload(负载)和signature(签名),每部分用 . 分隔,header.payload.signature

header 通常包含两部分:一个是alg(algorithm)指明算法类型;另一个是token的类型typ,可以是JWT也可以是其它RFC 7519实现的token机制,默认JWT----“typ”: “JWT”。
======举例:header经base64解码后得到"alg":“HS512”。这是HMAC采用HS512算法对JWT进行的签名。

payload 中可以放置三类数据:系统保留的、公共的和私有的:
======举例:“sub”:“wang”(用户名), “created”:1489079981393(创建时间), “exp”:1489684781(过期时间)

  • 系统保留的声明(Reserved claims):这类声明不是必须的,但是是建议使用的,包括:iss (签发者), exp (过期时间),
    sub (主题), aud (目标受众)等。这里我们发现都用的缩写的三个字符,这是由于JWT的目标就是尽可能小巧。
  • 公共声明:这类声明需要在 IANA JSON Web Token Registry 中定义或者提供一个URI,因为要避免重名等冲突。
  • 私有声明:这个就是你根据业务需要自己定义的数据了。

其中header、payload是公开的,是声明性质的,敏感信息谨慎存储,用base64解码即可获取

signature(签名)的过程是这样的:采用header中声明的算法,接受三个参数:base64编码的header、base64编码的payload和秘钥(secret)进行运算。对安全性要求高时也可以采用RSASHA256的方式进行公钥、私钥对的方式进行生成。

JWT的生成和解析

现在有很多成熟的类库可以使用,比如jjwt ( https://github.com/jwtk/jjwt ),可用于Java和Android的JWT token的生成和验证。
JWT的生成

/=放入用户名,可以简单的创建一个Map然后put进去就可以了。
====Map<String, Object> claims = new HashMap<>();
====claims.put(CLAIM_KEY_USERNAME, username());

String generateToken(Map<String, Object> claims) {
    return Jwts.builder()
            .setClaims(claims)
            .setExpiration(generateExpirationDate())
            /= 采用什么算法是可以自己选择的,不一定非要采用HS512
            .signWith(SignatureAlgorithm.HS512, secret) 
            .compact();
}

/=解析:利用 jjwt 提供的parser传入秘钥,然后就可以解析token了
Claims getClaimsFromToken(String token) {
    Claims claims;
    try {
        claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    } catch (Exception e) {
        claims = null;
    }
    return claims;
}

JWT提供了一种基于token的请求验证机制。
但用户权限,对于API的权限划分、资源的权限划分,用户的验证等等都不是JWT负责的。也就是说,请求验证后,你是否有权限看对应的内容是由你的用户角色决定的。所以我们这里要利用Spring的一个子项目Spring Security来简化我们的工作。

Spring Security

Spring Security是一个基于Spring的通用安全框架,这里不展开说明,如何利用Spring Security和JWT一起来完成API保护。ps:http://projects.spring.io/spring-security/ Spring Security基础知识。

铺垫
一个权限系统,一般是由用户表、角色表、用户角色对应表。如果对用户角色操作不多的话,可以融合成用户角色表。
采用lombok提供的@data修饰符省去构造方法和getter和setter

引入Spring Security

  • 添加依赖jjwt和security。
  • Spring Security需要我们实现几个接口,第一个接口是UserDetails
  • 创建一个简单的JwtUser类来实现这个UserDetails接口(完全是为了安全服务的)
public class JwtUser implements UserDetails {
    private final String id;
    private final String username;
    private final String password;
    private final String email;
    private final Collection<? extends GrantedAuthority> authorities;
    private final Date lastPasswordResetDate;

    public JwtUser(
            String id,
            String username,
            String password,
            String email,
            Collection<? extends GrantedAuthority> authorities,
            Date lastPasswordResetDate) {
        this.id = id;
        this.username = username;
        this.password = password;
        this.email = email;
        this.authorities = authorities;
        this.lastPasswordResetDate = lastPasswordResetDate;
    }
    //返回分配给用户的角色列表
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }
    
    @JsonIgnore
    public String getId() {
        return id;
    }

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

    @Override
    public String getUsername() {
        return username;
    }
    // 账户是否未过期
    @JsonIgnore
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    // 账户是否未锁定
    @JsonIgnore
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    // 密码是否未过期
    @JsonIgnore
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 账户是否激活
    @JsonIgnore
    @Override
    public boolean isEnabled() {
        return true;
    }
    // 这个是自定义的,返回上次密码重置日期
    @JsonIgnore
    public Date getLastPasswordResetDate() {
        return lastPasswordResetDate;
    }
}

写一个工厂类来由领域对象创建 JwtUser

public final class JwtUserFactory {

    private JwtUserFactory() {
    }

    public static JwtUser create(User user) {
        return new JwtUser(
                user.getId(),
                user.getUsername(),
                user.getPassword(),
                user.getEmail(),
                mapToGrantedAuthorities(user.getRoles()),
                user.getLastPasswordResetDate()
        );
    }

    private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) {
        return authorities.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
    }
}

第二个要实现的接口是 UserDetailsService

它定义了一个方法 loadUserByUsername,可以在数据库、文本文件、xml文件等地方根据用户名查到用户并返回。

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);

        if (user == null) {
            throw new UsernameNotFoundException(
            	String.format("No user found with username '%s'.", username));
        } else {
            return JwtUserFactory.create(user);
        }
    }
}

为了让Spring可以知道我们想怎样控制安全性,我们还需要建立一个安全配置类 WebSecurityConfig:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // Spring会自动寻找同样类型的具体类注入,这里就是JwtUserDetailsServiceImpl了
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                // 设置UserDetailsService
                .userDetailsService(this.userDetailsService)
                // 使用BCrypt进行密码的hash
                .passwordEncoder(passwordEncoder());
    }
    // 装载BCrypt密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
	        // 由于使用的是JWT,我们这里不需要csrf
            .csrf().disable()

            // 基于token,所以不需要session
            .sessionManagement()
            +.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

            .authorizeRequests()
            //.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()

            // 允许对于网站静态资源的无授权访问
            .antMatchers(
                HttpMethod.GET,
                   "/",
                   "/*.html",
                   "/favicon.ico",
                   "/**/*.html",
                   "/**/*.css",
                   "/**/*.js"
             ).permitAll()
             // 对于获取token的rest api要允许匿名访问
             .antMatchers("/auth/**").permitAll()
             // 除上面外的所有请求全部需要鉴权认证
             .anyRequest().authenticated();

        // 禁用缓存
        httpSecurity.headers().cacheControl();
    }
}

在 UserController 加一个修饰符 @PreAuthorize(“hasRole(‘ADMIN’)”) 表示这个资源只能被拥有 ADMIN 角色的用户访问。

/**
 * 在 @PreAuthorize 中我们可以利用内建的 SPEL 表达式:
 * 比如 'hasRole()' 来决定哪些用户有权访问。
 * 需注意的一点是 hasRole 表达式认为每个角色名字前都有一个前缀 'ROLE_'。
 * 所以这里的 'ADMIN' 其实在数据库中存储的是 'ROLE_ADMIN' 。
 * 这个 @PreAuthorize 可以修饰Controller也可修饰Controller中的方法。
 **/
@RestController
@RequestMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public class UserController {
    @Autowired
    private UserRepository repository;

    @RequestMapping(method = RequestMethod.GET)
    public List<User> getUsers() {
        return repository.findAll();
    }

    // 略去其它部分
}

到此,Spring Security可以工作了

application.yml配置logging,设置了数据库链接参数和http服务的一些配置项,服务器监听端口为8090,spring data和security的日志在debug模式下会输出到console。

application.yml
# Server configuration
server:
  port: 8090
  contextPath:

# Spring configuration
spring:
  jackson:
    serialization:
      INDENT_OUTPUT: true
  data.mongodb:
    host: localhost
    port: 27017
    database: springboot

# Logging configuration
logging:
  level:
    org.springframework:
      data: DEBUG
      security: DEBUG

访问 http://localhost:8090,然后再访问没有配置权限的路径,就会发现后台报错,由于用户未鉴权,被拒绝了。

2017-03-10 DEBUG 57599---[nio-8090-exec-4] o.s.s.w.a.ExceptionTranslationFilter     : 
Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: 
Access is denied at org.springframework.security.access.vote.AffirmativeBased.decide
(AffirmativeBased.java:84) ~[spring-security-core-4.2.1.RELEASE.jar:4.2.1.RELEASE]

集成JWT和Spring Security

要JWT在Spring中工作,新建一个filter,并把它配置在 WebSecurityConfig 中

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Value("${jwt.header}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain chain) throws ServletException, IOException {
        String authHeader = request.getHeader(this.tokenHeader);
        if (authHeader != null && authHeader.startsWith(tokenHead)) {
        	// The part after "Bearer "
            final String authToken = authHeader.substring(tokenHead.length()); 
            String username = jwtTokenUtil.getUsernameFromToken(authToken);

            logger.info("checking authentication " + username);

            if (username != null 
            	&& SecurityContextHolder.getContext().getAuthentication() == null) {

                UserDetails userDetails=this.userDetailsService.loadUserByUsername(username);

                if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                
                    UsernamePasswordAuthenticationToken authentication = 
                    	new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(
                    		new WebAuthenticationDetailsSource().buildDetails(request));
                    
                    logger.info("authenticated user " + username + ", setting security context");
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        chain.doFilter(request, response);
    }
}

如果token中的数据足够可信可用,就可以不用查询宝贵的数据库了,直接鉴定token是否合法

接着,在 WebSecurityConfig 中注入这个filter,并且配置到 HttpSecurity 中

public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

    // 省略其它部分

    @Bean
    public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 省略之前写的规则部分,具体看前面的代码

        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilterBean(),
        									UsernamePasswordAuthenticationFilter.class);
    }
}

完成鉴权(登录)、注册和更新token的功能

到现在,我们整个API其实已经在安全的保护下了,但我们遇到一个问题:所有的API都安全了,但我们还没有用户啊,所以所有API都没法访问。因此要提供一个注册、登录的API,这个API应该是可以匿名访问的。给它规划的路径呢,我们前面其实在WebSecurityConfig中已经给出了,就是 /auth。

AuthService

public interface AuthService {
    User register(User userToAdd);
    String login(String username, String password);
    String refresh(String oldToken);
}

然后,实现这些必选动作,其实非常简单:

登录时要生成token,完成Spring Security认证,然后返回token给客户端
注册时将用户密码用BCrypt加密,写入用户角色,由于是开放注册,所以写入角色系统控制,将其写成 ROLE_USER

提供一个可以刷新token的接口 refresh 用于取得新的token

@Service
public class AuthServiceImpl implements AuthService {

    private AuthenticationManager authenticationManager;
    private UserDetailsService userDetailsService;
    private JwtTokenUtil jwtTokenUtil;
    private UserRepository userRepository;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Autowired
    public AuthServiceImpl(
            AuthenticationManager authenticationManager,
            UserDetailsService userDetailsService,
            JwtTokenUtil jwtTokenUtil,
            UserRepository userRepository) {
        this.authenticationManager = authenticationManager;
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
        this.userRepository = userRepository;
    }

    @Override
    public User register(User userToAdd) {
        final String username = userToAdd.getUsername();
        if(userRepository.findByUsername(username)!=null) {
            return null;
        }
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        final String rawPassword = userToAdd.getPassword();
        userToAdd.setPassword(encoder.encode(rawPassword));
        userToAdd.setLastPasswordResetDate(new Date());
        userToAdd.setRoles(asList("ROLE_USER"));
        return userRepository.insert(userToAdd);
    }

    @Override
    public String login(String username, String password) {
        UsernamePasswordAuthenticationToken upToken = 
        					new UsernamePasswordAuthenticationToken(username, password);
        final Authentication authentication = authenticationManager.authenticate(upToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        final UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        final String token = jwtTokenUtil.generateToken(userDetails);
        return token;
    }

    @Override
    public String refresh(String oldToken) {
        final String token = oldToken.substring(tokenHead.length());
        String username = jwtTokenUtil.getUsernameFromToken(token);
        JwtUser user = (JwtUser) userDetailsService.loadUserByUsername(username);
        if (jwtTokenUtil.canTokenBeRefreshed(token, user.getLastPasswordResetDate())){
            return jwtTokenUtil.refreshToken(token);
        }
        return null;
    }
}

然后建立AuthController就好,这个AuthController中我们在其中使用了表达式绑定,比如 @Value("${jwt.header}")中的 jwt.header 其实是定义在 applicaiton.yml 中的

# JWT
jwt:
  header: Authorization
  secret: mySecret
  expiration: 604800
  tokenHead: "Bearer "
  route:
    authentication:
      path: auth
      refresh: refresh
      register: "auth/register"

同样的 @RequestMapping(value = “${jwt.route.authentication.path}”, method = RequestMethod.POST) 中的 jwt.route.authentication.path 也是定义在上面的

@RestController
public class AuthController {
    @Value("${jwt.header}")
    private String tokenHeader;

    @Autowired
    private AuthService authService;

    @RequestMapping(value = "${jwt.route.authentication.path}", method = RequestMethod.POST)
    public ResponseEntity<?> createAuthenticationToken(
            @RequestBody JwtAuthenticationRequest authenticationRequest) throws AuthenticationException{
        final String token = authService.login(authenticationRequest.getUsername(), authenticationRequest.getPassword());

        // Return the token
        return ResponseEntity.ok(new JwtAuthenticationResponse(token));
    }

    @RequestMapping(value = "${jwt.route.authentication.refresh}", method = RequestMethod.GET)
    public ResponseEntity<?> refreshAndGetAuthenticationToken(
            HttpServletRequest request) throws AuthenticationException{
        String token = request.getHeader(tokenHeader);
        String refreshedToken = authService.refresh(token);
        if(refreshedToken == null) {
            return ResponseEntity.badRequest().body(null);
        } else {
            return ResponseEntity.ok(new JwtAuthenticationResponse(refreshedToken));
        }
    }

    @RequestMapping(value = "${jwt.route.authentication.register}", method = RequestMethod.POST)
    public User register(@RequestBody User addedUser) throws AuthenticationException{
        return authService.register(addedUser);
    }
}

验证

ADMIN角色或者是当前用户本身。Spring Security真是很方便,很强大。

@PostAuthorize("returnObject.username == principal.username or hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/",method = RequestMethod.GET)
public User getUserByUsername(@RequestParam(value="username") String username) {
    return repository.findByUsername(username);
}

作者:接灰的电子产品
链接:https://www.jianshu.com/p/6307c89fe3fa
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值