leftso 6273 0 2018-09-02 16:23:39

文章位置:左搜> 编程技术> 正文

引言

    在这篇文章中,我们将通过JWT(JSOn Web Token)认证来保护我们的REST API 。我们将使用基于spring boot maven的配置来开发并保护我们的API,并提供单独的API用于注册并生成令牌。我们将扩展OncePerRequestFilter类,以使用JWT定义我们的自定义认证机制。认证机制可以应用于URL和方法。最后,我们将使用谷歌高级REST客户端测试实现。

 

项目结构

以下是我们将为spring  boot JWT认证而建立的最终项目结构。
项目结构图
 

JWT认证机制

以下类继承了OncePerRequestFilter,确保每个请求调度都有一次执行。该类检查授权头并验证JWT令牌并在上下文中设置验证。这样做可以保护我们的API免受那些没有任何授权令牌的请求。有关哪些资源需要保护以及哪些不可以配置WebSecurityConfig.java

JwtAuthenticationFilter.java
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
        String header = req.getHeader(HEADER_STRING);
        String username = null;
        String authToken = null;
        if (header != null && header.startsWith(TOKEN_PREFIX)) {
            authToken = header.replace(TOKEN_PREFIX,"");
            try {
                username = jwtTokenUtil.getUsernameFromToken(authToken);
            } catch (IllegalArgumentException e) {
                logger.error("an error occured during getting username from token", e);
            } catch (ExpiredJwtException e) {
                logger.warn("the token is expired and not valid anymore", e);
            } catch(SignatureException e){
                logger.error("Authentication Failed. Username or Password not valid.");
            }
        } else {
            logger.warn("couldn't find bearer string, will ignore the header");
        }
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {

            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtTokenUtil.validateToken(authToken, userDetails)) {
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
                logger.info("authenticated user " + username + ", setting security context");
                SecurityContextHolder.getContext().setAuthentication(authentication);
            }
        }

        chain.doFilter(req, res);
    }
}
以下是用于生成身份验证令牌以及从令牌中提取用户名的util类。这里是我们需要url的配置,例如/ token / */ signup / *是公开可用的,其余url是受限于公共访问。
JwtTokenUtil.java
@Component
public class JwtTokenUtil implements Serializable {

    public String getUsernameFromToken(String token) {
        return getClaimFromToken(token, Claims::getSubject);
    }

    public Date getExpirationDateFromToken(String token) {
        return getClaimFromToken(token, Claims::getExpiration);
    }

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

    private Claims getAllClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SIGNING_KEY)
                .parseClaimsJws(token)
                .getBody();
    }

    private Boolean isTokenExpired(String token) {
        final Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }

    public String generateToken(User user) {
        return doGenerateToken(user.getUsername());
    }

    private String doGenerateToken(String subject) {

        Claims claims = Jwts.claims().setSubject(subject);
        claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN")));

        return Jwts.builder()
                .setClaims(claims)
                .setIssuer("http://devglan.com")
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
                .signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
                .compact();
    }

    public Boolean validateToken(String token, UserDetails userDetails) {
        final String username = getUsernameFromToken(token);
        return (
              username.equals(userDetails.getUsername())
                    && !isTokenExpired(token));
    }

}
以下是我们在上面的实现中使用的常量。
Constants.java
public class Constants {

    public static final long ACCESS_TOKEN_VALIDITY_SECONDS = 5*60*60;
    public static final String SIGNING_KEY = "devglan123r";
    public static final String TOKEN_PREFIX = "Bearer ";
    public static final String HEADER_STRING = "Authorization";
}
 

Spring Boot安全配置

现在让我们定义我们通常的spring引导安全配置。我们注入了userDetailsS​​ervice以从数据库中获取用户凭据。
在这里,注释@EnableGlobalMethodSecurity启用了方法级别的安全性,您可以使用注释(例如@Secured)注释您的方法,以在方法级别提供基于角色的身份验证。

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

    @Resource(name = "userService")
    private UserDetailsService userDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

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

    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(encoder());
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().
                authorizeRequests()
                .antMatchers("/token/*").permitAll()
                .anyRequest().authenticated()
                .and()
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http
                .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    public BCryptPasswordEncoder encoder(){
        return new BCryptPasswordEncoder();
    }

}

以下是暴露在代表用户创建令牌的控制器,如果您注意到WebSecurityConfig.java我们已将此url配置为不进行身份验证,以便用户可以使用有效凭据生成JWT令牌。

AuthenticationController.java
@RestController
@RequestMapping("/token")
public class AuthenticationController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Autowired
    private UserService userService;

    @RequestMapping(value = "/generate-token", method = RequestMethod.POST)
    public ResponseEntity register(@RequestBody LoginUser loginUser) throws AuthenticationException {

        final Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginUser.getUsername(),
                        loginUser.getPassword()
                )
        );
        SecurityContextHolder.getContext().setAuthentication(authentication);
        final User user = userService.findOne(loginUser.getUsername());
        final String token = jwtTokenUtil.generateToken(user);
        return ResponseEntity.ok(new AuthToken(token));
    }

}
我们有非常简单的REST Apis公开测试用途。以下是实现。
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @RequestMapping(value="/user", method = RequestMethod.GET)
    public List listUser(){
        return userService.findAll();
    }

    @RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
    public User getOne(@PathVariable(value = "id") Long id){
        return userService.findById(id);
    }

}

以下是我们的实体类。
User.java
@Entity
public class User {

    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private long id;
    @Column
    private String username;
    @Column
    @JsonIgnore
    private String password;
    @Column
    private long salary;
    @Column
    private int age;
}

默认脚本

以下是在应用程序启动时插入的插入语句。

INSERT INTO User (id, username, password, salary, age) VALUES (1, 'Alex123', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 3456, 33);
INSERT INTO User (id, username, password, salary, age) VALUES (2, 'Tom234', '$2a$04$PCIX2hYrve38M7eOcqAbCO9UqjYg7gfFNpKsinAxh99nms9e.8HwK', 7823, 23);
INSERT INTO User (id, username, password, salary, age) VALUES (3, 'Adam', '$2a$04$I9Q2sDc4QGGg5WNTLmsz0.fvGv3OjoZyj81PrSFyGOqMphqfS2qKu', 4234, 45);

JWT用户登录

由于我们有默认脚本来预先填充数据库中的数据以供测试,但我们也可以为用户注册公开一个API。使用此API用户可以注册并使用相同的用户名和密码来生成令牌。为此,我们在控制器类中添加了以下方法。
@RequestMapping(value="/signup", method = RequestMethod.POST)
public User saveUser(@RequestBody UserDto user){
	return userService.save(user);
}
一旦添加,我们需要删除此URL的限制以供公众访问。为此,请在我们的Spring Boot security config中添加以下行。
.antMatchers("/token/*", "/signup").permitAll()

现在为了创建用户,我们在UserServiceImpl.java保存数据库中的用户记录方面做了简单的实现。这里要注意的一点是使用bcrypt编码器的密码加密。我们已经自动装配了我们定义为bean的相同编码器WebSecurityConfig.java
 

@Autowired
private BCryptPasswordEncoder bcryptEncoder;

@Override
public User save(UserDto user) {
	User newUser = new User();
	newUser.setUsername(user.getUsername());
	newUser.setPassword(bcryptEncoder.encode(user.getPassword()));
	newUser.setAge(user.getAge());
	newUser.setSalary(user.getSalary());
	return userDao.save(newUser);
}

这将显示用户注册过程的以下URL以生成JWT令牌。
配置postmain

 

测试应用程序

我们将使用Advanced REST Client来测试spring boot jwt认证。

使用令牌生成AuthToken 访问资源,而不使用令牌访问资源

3
4
5

项目源码下载
原文地址:http://www.devglan.com/spring-security/spring-boot-jwt-auth

评论区域

暂无评论,快来抢首发吧!!!