SpringBoot+JWT+Spring Security实现登录鉴权

1、配置pom文件,添加依赖


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


    io.jsonwebtoken
    jjwt
    0.9.1

    

2、配置application.properties文件

#JWT
# 签发者
jwt.issuer=Augmentum
jwt.header=Authorization
# 过期时间(-1永不过期)
jwt.expires_in=-1
#jwt.expires_in=86400
# 密匙
jwt.secret=gvdlju
jwt.cookie=AUTH-TOKEN

3、项目结构如下

SpringBoot+JWT+Spring Security实现登录鉴权_第1张图片

4、添加用户实体类以及相关操作的类

  • model实体类
/**
 * @author user
 */
@Entity
@Table(name = "tb_user")
public class User {

    @Id
    @GeneratedValue
    private long id;
    private String username;
    private String password;

    public long getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}
  • repository
/**
 * The interface User repository.
 *
 * @author user
 */
@Repository
public interface UserRepository extends JpaRepository {

    User findByUsername(String username);

    @Query(value = "select user from User user where user.username=?1 and user.password=?2")
    User findByUsernameAndPassword(String username, String password);
}
  • service--提供用户注册和用户密码验证
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserRepository userRepository;

    @Override
    public User findByUserName(String userName) {
        return userRepository.findByUsername(userName);
    }

    @Override
    public User findByUserNameAndPassword(String userName, String password) {
        return userRepository.findByUsernameAndPassword(userName, password);
    }

    @Override
    public User signUp(User user) {
        return userRepository.save(user);
    }
}

5、创建TokenHelper工具类生成/解析Token

@Component
public class TokenHelper {

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

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

    @Value("${jwt.expires_in}")
    private int EXPIRES_IN;

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

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

    @Autowired
    UserService userService;

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    // 数字签名算法
    private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;

    /**
     * 从Token中取得用户名
     * @param token
     * @return
     * @throws TokenInvalidException
     * @throws TokenException
     */
    public String getUsernameFromToken(String token) throws TokenInvalidException, TokenException {
        final Claims claims = this.getClaimsFromToken(token);
        String username = claims.getSubject();
        return username;
    }

    /**
     * 根据用户名生成Token
     * @param username
     * @return
     */
    public String generateToken(String username) {
        return Jwts.builder().setIssuer(ISSUER).setSubject(username).setIssuedAt(generateCurrentDate())
                .setExpiration(generateExpirationDate()).signWith(SIGNATURE_ALGORITHM, SECRET).compact();
    }

    private Claims getClaimsFromToken(String token) throws TokenInvalidException, TokenException {
        Claims claims;
        try {
            claims = Jwts.parser().setSigningKey(this.SECRET).parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException e) {
            logger.error(e.getMessage(), e);
            throw new TokenException(ExceptionEnum.TOKEN_ERROR_EXPIRED.getCode(), ExceptionEnum.TOKEN_ERROR_EXPIRED.getMessage());
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            throw new TokenInvalidException(token);
        }
        return claims;
    }

    String generateToken(Map claims) {
        return Jwts.builder().setClaims(claims).setExpiration(generateExpirationDate())
                .signWith(SIGNATURE_ALGORITHM, SECRET).compact();
    }

    public Boolean canTokenBeRefreshed(String token) {
        try {
            final Date expirationDate = getClaimsFromToken(token).getExpiration();
            String username = getUsernameFromToken(token);
            User user = userService.findByUserName(username);

            if (user != null && expirationDate.compareTo(generateCurrentDate()) > 0) {
                return true;
            }
            return false;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return false;
        }
    }

    public String refreshToken(String token) {
        String refreshedToken;
        try {
            final Claims claims = getClaimsFromToken(token);
            claims.setIssuedAt(generateCurrentDate());
            refreshedToken = generateToken(claims);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            refreshedToken = null;
        }
        return refreshedToken;
    }

    private long getCurrentTimeMillis() {
        // return DateTime.now().getMillis();
        return System.currentTimeMillis();
    }

    private Date generateCurrentDate() {
        return new Date(getCurrentTimeMillis());
    }

    private Date generateExpirationDate() {
        if ( this.EXPIRES_IN == -1) {
            return null;
        }
        logger.info("EXPIRES_IN" + EXPIRES_IN);
        logger.info(new Date(getCurrentTimeMillis() + this.EXPIRES_IN * 1000) + "///");
        return new Date(getCurrentTimeMillis() + this.EXPIRES_IN * 1000);
    }

    public String getToken(HttpServletRequest request) {
        /**
         * Getting the token from Cookie store
         */

        Cookie authCookie = getCookieValueByName(request, AUTH_COOKIE);
        if (authCookie != null) {
            return authCookie.getValue();
        }
        /**
         * Getting the token from Authentication header e.g Bearer your_token
         */
        String authHeader = request.getHeader(AUTH_HEADER);
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }

        return null;
    }

    /**
     * Find a specific HTTP cookie in a request.
     *
     * @param request
     *            The HTTP request object.
     * @param name
     *            The cookie name to look for.
     * @return The cookie, or null if not found.
     */
    public Cookie getCookieValueByName(HttpServletRequest request, String name) {
        if (request.getCookies() == null) {
            return null;
        }
        for (int i = 0; i < request.getCookies().length; i++) {
            if (request.getCookies()[i].getName().equals(name)) {
                return request.getCookies()[i];
            }
        }
        return null;
    }

    /**
     * 将User转换为JwtUser
     * @param user
     * @return
     */
    public JwtUser convertUserToJwtUser(User user) {
        JwtUser jwtUser = new JwtUser(user.getId(), user.getUsername(), getAuthorities(user));

        return jwtUser;
    }

    /**
     * 得到User的权限
     * @param user
     * @return
     */
    public List getAuthorities(User user) {
        List authorities = new ArrayList<>();

        // 添加权限--这里添加了admin权限
        authorities.add(new AuthorityInfo(Authority.ADMIN.getAuthorityName()));

        return authorities;
    }
}

6、实现GrantedAuthority接口,设置用户权限

  • AuthorityInfo
public class AuthorityInfo implements GrantedAuthority {

    private static final long serialVersionUID = 1L;

    private String authority;

    public AuthorityInfo(String authority) {
        this.authority = authority;
    }

    @Override
    public String getAuthority() {
        return authority;
    }

    public void setAuthority(String authority) {
        this.authority = authority;
    }
}
  • Authority定义权限分类--Admin和User
public enum Authority {
    ADMIN("admin"), USER("user");

    private String authorityName;

    Authority(String authorityName) {
        this.authorityName = authorityName;
    }

    public String getAuthorityName() {
        return this.authorityName;
    }
}

7、实现UserDetailsService接口返回UserDetails用户信息

  • JwtUser--自定义UserDetails
public class JwtUser implements UserDetails {

    private static final long serialVersionUID = 1L;

    private long id;
    private String userName;
    private List authorities;

    @Override
    public Collection getAuthorities() {
        return this.authorities;
    }

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

    @Override
    public String getUsername() {
        return this.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 JwtUser(long id, String userName, List authorities) {
        super();
        this.id = id;
        this.userName = userName;
        this.authorities = authorities;
    }

    public JwtUser() {
        super();
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public void setAuthorities(List authorities) {
        this.authorities = authorities;
    }
}
  • JwtCustomUserDetailsService--自定义UserDetailsService
@Service
public class JwtCustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TokenHelper tokenHelper;

    @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 tokenHelper.convertUserToJwtUser(user);
        }
    }
}

8、过滤Token,认证UserDetails并生成认证通过的Authentication存放进SecurityContextHolder

  • TokenAuthenticationFilter
/**
 * Token拦截器,token过滤器来验证token有效性
 * 从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。
 * 如果校验通过,就认为这是一个取得授权的合法请求
 * @author user
 */
public class TokenAuthenticationFilter extends OncePerRequestFilter {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    TokenHelper tokenHelper;

    @Autowired
    JwtCustomUserDetailsService jwtCustomUserDetailsService;

    @Autowired
    HttpUtil httpUtil;

    /**
     * 对请求进行过滤,验证token的有效性,
     * 如果有效将用户信息存入spring security context中
     */
    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        String authToken = tokenHelper.getToken(request);

        if (StringUtils.isEmpty(authToken) || authToken.equals("null")) {
            //authToken为空的处理
            TokenException tokenEmptyException = new TokenException(ExceptionEnum.TOKEN_ERROR_EMPTY.getCode(), ExceptionEnum.TOKEN_ERROR_EMPTY.getMessage());
            httpUtil.returnResponseBody(request, response, tokenEmptyException);
            return;
        }
        logger.info("authToken:::" + authToken);

        String username = null;
        try {
            username = tokenHelper.getUsernameFromToken(authToken);
        } catch (TokenException e) {
            logger.error(e.getMessage(), e);
            TokenException tokenExpiredException = new TokenException(ExceptionEnum.TOKEN_ERROR_EXPIRED.getCode(), ExceptionEnum.TOKEN_ERROR_EXPIRED.getMessage());
            httpUtil.returnResponseBody(request, response, tokenExpiredException);
            return;
        } catch (TokenInvalidException e) {
            logger.error(e.getMessage(), e);
            TokenInvalidException tokenInvalidException = new TokenInvalidException(authToken);
            httpUtil.returnResponseBody(request, response, tokenInvalidException);
            return;
        }
        UserDetails userDetails = jwtCustomUserDetailsService.loadUserByUsername(username);
        if (userDetails == null) {
            logger.error("Invalid Token error with username: " + username);
            TokenInvalidException tokenInvalidException = new TokenInvalidException(authToken);
            httpUtil.returnResponseBody(request, response, tokenInvalidException);
            return;
        }
        // create authentication
        TokenBasedAuthentication authentication = new TokenBasedAuthentication(userDetails);
        authentication.setToken(authToken);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        chain.doFilter(request, response);
    }

    /**
     * 配置不经过filter的请求
     */
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        final String APP_REST_MATCHER = "/oauth/**";

        final String SWAGGER_UI = "/swagger-ui.html";
        final String SWAGGER_RESOURCES = "/swagger-resources/**";
        final String SWAGGER_IMAGES = "/images/**";
        final String SWAGGER_WEBJARS = "/webjars/**";
        final String SWAGGER_V2 = "/v2/api-docs";
        final String SWAGGER_CONF_UI = "/configuration/ui";
        final String SWAGGER_CONF_SEC = "/configuration/security";

        List pathsToSkip = Arrays.asList(APP_REST_MATCHER, SWAGGER_UI, SWAGGER_RESOURCES, SWAGGER_IMAGES, SWAGGER_WEBJARS, SWAGGER_V2, SWAGGER_CONF_UI, SWAGGER_CONF_SEC);

        // web端api不需要验证token,只需走cas逻辑(如请求满足跳过验证条件)
        return skipPathRequest(request, pathsToSkip);
    }

    /**
     * 判断请求是否跳过验证
     * @param request 用户发送的请求
     * @param pathsToSkip 可跳过验证的请求
     * @return
     */
    private boolean skipPathRequest(HttpServletRequest request, List pathsToSkip) {
        Assert.notNull(pathsToSkip, "pathsToSkip is null!");
        List m = pathsToSkip.stream().map(path -> new AntPathRequestMatcher(path))
                .collect(Collectors.toList());
        OrRequestMatcher matchers = new OrRequestMatcher(m);
        return matchers.matches(request);
    }
}
  • TokenBasedAuthentication -- 自定义AbstractAuthenticationToken
public class TokenBasedAuthentication extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private String token;
    private final UserDetails principle;

    public TokenBasedAuthentication(UserDetails principle ) {
        super( principle.getAuthorities() );
        this.principle = principle;
    }

    public String getToken() {
        return token;
    }

    public void setToken(String token ) {
        this.token = token;
    }

    @Override
    public boolean isAuthenticated() {
        return true;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    @Override
    public UserDetails getPrincipal() {
        return principle;
    }
}

9、自定义认证失败处理类,然后配置Security的认证策略

  • RestAuthenticationEntryPoint--认证失败处理类
/**
 * 认证失败处理类
 * @author user
 */
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
    }
}
  • WebSecurityConfig--认证策略配置
/**
 * SpringSecurity的配置
 * 配置Security的认证策略
 * @author user
 */
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public TokenAuthenticationFilter jwtAuthenticationTokenFilter() throws Exception {
        return new TokenAuthenticationFilter();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private JwtCustomUserDetailsService customUserDetailsService;

    @Autowired
    private RestAuthenticationEntryPoint restAuthenticationEntryPoint;

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
            // 由于使用的是JWT,我们这里不需要csrf
            .csrf().disable()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().
            // 添加认证失败处理类
            exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint).and()
            .authorizeRequests()
            // 对于获取token的rest api要允许匿名访问
            .antMatchers("/oauth/**").permitAll()
            .antMatchers("/static/**").permitAll()
            // swagger start
            .antMatchers("/swagger-ui.html").permitAll()
            .antMatchers("/swagger-resources/**").permitAll()
            .antMatchers("/images/**").permitAll()
            .antMatchers("/webjars/**").permitAll()
            .antMatchers("/v2/api-docs").permitAll()
            .antMatchers("/configuration/ui").permitAll()
            .antMatchers("/configuration/security").permitAll()
            // swagger end
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated();
        http
            // 添加JWT filter
            .addFilterBefore(jwtAuthenticationTokenFilter(), BasicAuthenticationFilter.class);
    }
}

10、Controller层测试

  • 无需验证token的映射--注册登录
@RestController
@RequestMapping(value = "/oauth")
public class OauthController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private TokenHelper tokenHelper;

    @Autowired
    private UserService userService;

    /**
     * 注册用户,无需验证token
     * @param user
     */
    @PostMapping("/signup")
    public void signUp(@RequestBody User user) {
        userService.signUp(user);
    }

    /**
     * 登录并返回jwt生成的token
     * @param user
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public JsonResponse loginWeChat(@RequestBody User user) throws Exception {
        User existUser = userService.findByUserNameAndPassword(user.getUsername(), user.getPassword());
        if (existUser == null) {
            throw new UserNameOrPasswordIncorrectException();
        }

        logger.info("登录:::" + user.getUsername());
        String token = tokenHelper.generateToken(user.getUsername());

        return new JsonResponse(true, token);
    }
}
  • 需要token的映射
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/test")
    public String test() {
        System.out.println("test");
        return "hello";
    }
}

请求验证:访问/users/test

  • 不带token访问

SpringBoot+JWT+Spring Security实现登录鉴权_第2张图片

  • 携带无效token访问

SpringBoot+JWT+Spring Security实现登录鉴权_第3张图片

  • 登录生成token

SpringBoot+JWT+Spring Security实现登录鉴权_第4张图片

  • 携带生成的有效token进行访问--成功返回hello

SpringBoot+JWT+Spring Security实现登录鉴权_第5张图片

你可能感兴趣的