spring boot 集成jwt security
文章結(jié)構(gòu)
開門見山
數(shù)據(jù)流程
開門見山
這一部分直接展示代碼,及將哪些代碼進行修改就可以直接移值到自己的項目進行安全驗證
代碼目錄結(jié)構(gòu)
Auth
AuthController
LoginUser
JWT
JwtUtil
Security
AuthFilter
SecurityConfig
UserDetailsImpl
UserDetailsServiceImpl
User
UserController
UserService
User
UserRepository
AuthController
說明:該類是自定義的用戶進行登錄驗證獲取token 的接口,是所有人都能訪問的
package demo.demo1.auth.api.rest;
import demo.demo1.User.model.User;
import demo.demo1.User.service.UserService;
import demo.demo1.auth.jwt.JwtUtil;
import demo.demo1.auth.model.LoginUser;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
@RestController
@RequestMapping("/auth")
@CrossOrigin(origins = "*")
@Api(
value = "/auth",
description = "用戶登錄認證"
)
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AuthenticationManager authenticationManager;
@RequestMapping(value = "", method = RequestMethod.POST)
@ApiOperation(
value = "登錄",
produces = "application/json"
)
public void login(
@ApiParam(value = "登錄用戶名/密碼", name = "LoginUser", required = true)
@Validated
@RequestBody LoginUser loginUser,
HttpServletResponse response) throws Exception {
try {
/** 通過security驗證登錄賬號是否正確,這里直接將用戶和密碼傳入security就好,
* 不需要在這里進行驗證,你的驗證會在userDetailService中由security幫你進行
*/
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginUser.getUsername(),
loginUser.getPassword()
)
);
} catch (AuthenticationException e) {
throw new Exception("Username or Password error.");
}
// 驗證通過后返回一個token值在http head中
User user = userService.getUserByUserName(loginUser.getUsername());
String token = jwtUtil.generateToken(user);
// set token to header
response.setHeader(JwtUtil.HEADER_STRING, token);
}
}
LoginUser
說明: 該類是你進行登錄時的bean,這里主要就是為了和user進行區(qū)分,登錄的時候用這個bean
package demo.demo1.auth.model;
import io.swagger.annotations.ApiModel;
import javax.validation.constraints.NotNull;
@ApiModel(value = "Login User", description = "登錄用戶信息")
public class LoginUser {
@NotNull
private String username;
@NotNull
private String password;
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;
}
}
JwtUtil
說明:這個類就是依據(jù)你的登錄用戶生成jwt token,驗證/解析請求時的token
package demo.demo1.auth.jwt;
import demo.demo1.User.model.User;
import demo.demo1.User.service.UserService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@Component
public class JwtUtil {
/**
token前綴
*/
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 設(shè)置http head中Authorization字段為token
*/
public static final String HEADER_STRING = "Authorization";
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
@Autowired
private UserService userService;
/**
* 依據(jù)登錄的賬號生成token
*/
public String generateToken(User user) throws Exception {
if (user == null || user.getId() == null) {
throw new Exception(String.format("user %s not valid", user));
}
//設(shè)置token參數(shù)
Map<String, Object> claims = new HashMap<>();
claims.put("sub", user.getId());
claims.put("aud", "web");
claims.put("iss", "demo");
claims.put("iat", new Date());
return JwtUtil.TOKEN_PREFIX + Jwts.builder()
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 解析token
*/
public Claims parseTokenClaims(String token) throws Exception {
try {
String pure = token.replace(JwtUtil.TOKEN_PREFIX, "");
return Jwts.parser().setSigningKey(secret).parseClaimsJws(pure).getBody();
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
/**
* 驗證token
*/
public Boolean validateToken(String token) {
try {
String pure = token.replace(JwtUtil.TOKEN_PREFIX, "");
Claims claims = parseTokenClaims(pure);
String subject = claims.getSubject();
User user = userService.getUserById(UUID.fromString(subject));
if (user == null) {
return false;
} else if (claims.getExpiration().after(new Date())) {
return true;
}
return false;
} catch (Exception e) {
}
return false;
}
}
AuthFilter
說明
這個類繼承了OncePerRequestFilter
當(dāng)發(fā)送一個攜帶token的http請求訪問某個接口的時候,這個過濾器就進行驗證其用戶權(quán)限
該類重寫了doFilterInternal方法,在該方法中通過token進行權(quán)限驗證
package demo.demo1.auth.security;
import demo.demo1.User.service.UserService;
import demo.demo1.auth.jwt.JwtUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
@Component
public class AuthFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private UserService userService;
@Autowired
private Environment env;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
String token = request.getHeader(JwtUtil.HEADER_STRING);
if (token != null && token.startsWith(JwtUtil.TOKEN_PREFIX)) {
token = token.replace(JwtUtil.TOKEN_PREFIX, "");
try {
String id = jwtUtil.parseTokenClaims(token).getSubject();
String username = userService.getUserById(UUID.fromString(id)).getUsername();
if (null != id && SecurityContextHolder.getContext().getAuthentication() == null) {
logger.debug("Checking token for user {}", id);
// In security, use uuid as username.
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(token) && userDetails != null) {
// create authentication
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
// set authentication
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// put authentication into context holder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
} catch (Exception e) {
logger.debug("Check token failed {}", e.getMessage());
}
}
chain.doFilter(request, response);
}
}
SecurityConfig
說明:該類是security的配置類,通過該類可以控制資源訪問權(quán)限,通過什么方式進行驗證用戶權(quán)限
package demo.demo1.auth.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public AuthFilter authorizationFilterBean() throws Exception {
return new AuthFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 禁用csrf
.csrf().disable()
// 因為是用的jwt所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 運行auth路徑訪問
.antMatchers("/auth").permitAll()
// 設(shè)置允許訪問的資源
.antMatchers("/webjars/**").permitAll()
.antMatchers(
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/swagger-ui.html",
"/v2/*",
"/user"
).permitAll()
.anyRequest().authenticated();
// 設(shè)置security過濾器
http
.addFilterBefore(authorizationFilterBean(), UsernamePasswordAuthenticationFilter.class);
http.headers().cacheControl();
}
/**
* 設(shè)置用戶權(quán)限驗證方式
*/
@Override
protected void configure(AuthenticationManagerBuilder amb) throws Exception {
amb.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
// 裝載BCrypt密碼編碼器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserDetailsImpl
說明:該類實現(xiàn)了security的UserDetails,進行自定義驗證用戶驗證用戶
值得注意的是,在轉(zhuǎn)換的時候user role前綴必須為ROLE_,否則security會返回403狀態(tài)碼(我在這炸了一天)
package demo.demo1.auth.security;
import demo.demo1.User.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class UserDetailsImpl implements UserDetails {
private User user;
public UserDetailsImpl(User user) {
this.user = user;
}
//將你自定義的用戶角色轉(zhuǎn)換為security的user role
//值得注意的是,在轉(zhuǎn)換的時候user role前綴必須為ROLE_,否則security會返回403狀態(tài)碼
private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList());
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
List<String> roles = new ArrayList<String>() {{ add(user.getRole());}};
if (roles == null) {
roles = new ArrayList<String>();
}
return mapToGrantedAuthorities(roles);
}
//必須要有,可以自定義,判斷用戶是否被禁用
@Override
public boolean isEnabled() { return true; };
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public String getUsername() {
return user.getUsername();
}
}
UserDetailsServiceImpl
說明:該類繼承了UserDetailsService,自定義security用戶驗證,只需要實現(xiàn)loadUserByUsername方法
該方法正常寫法就如下所示,一般不需要修改
package demo.demo1.auth.security;
import demo.demo1.User.model.User;
import demo.demo1.User.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final static Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
User user = userService.getUserByUserName(username);
if (user != null) {
return new UserDetailsImpl(user);
} else {
throw new UsernameNotFoundException("username not found.");
}
} catch (UsernameNotFoundException e) {
throw e;
} catch (Exception e) {
logger.error(e.getMessage());
throw new UsernameNotFoundException(e.getMessage());
}
}
}
UserController
說明:用來驗證security的一個例子
使用@PreAuthorize("hasRole('ROLE_SENIOR')")注解限制只能ROLE_SENIOR權(quán)限的用戶訪問該接口
這里post接口沒做限制方便實驗的時候可以自定義用戶
package demo.demo1.User.controller;
import demo.demo1.User.model.User;
import demo.demo1.User.service.UserService;
import demo.demo1.auth.security.SecurityConfig;
import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.UUID;
@RestController
@RequestMapping("/user")
@Api(
value = "/user",
description = "用戶API"
)
public class UserController {
@Autowired
UserService userService;
@Autowired
SecurityConfig securityConfig;
@RequestMapping(value = "" , method = RequestMethod.GET)
@PreAuthorize("hasRole('ROLE_SENIOR')")
@ApiOperation(
value = "get all User",
code = 201,
consumes = "application/json",
produces = "application/json"
)
public List<User> getAllUser() {
return userService.getAllUser();
}
@RequestMapping(value = "/{id}" , method = RequestMethod.GET)
@PreAuthorize("hasRole('ROLE_SENIOR')")
@ApiOperation(
value = "get one user by user id",
code = 201,
consumes = "application/json",
produces = "application/json"
)
public User getOneUser(
@ApiParam(value = "用戶UUID") @PathVariable UUID id
) {
return userService.getUserById(id);
}
@RequestMapping(value = "" , method = RequestMethod.POST)
//@PreAuthorize("hasRole('ROLE_SENIOR')")
@ApiOperation(
value = "create user",
code = 201,
consumes = "application/json",
produces = "application/json"
)
public void create(
@RequestBody User user
) throws Exception{
user.setUsername(user.getUsername().trim());
user.setPassword(user.getPassword().trim());
user.setId(UUID.randomUUID());
user.setEmail(user.getEmail().trim());
// encode password
BCryptPasswordEncoder passwordEncoder = (BCryptPasswordEncoder) securityConfig.passwordEncoder();
user.setPassword(passwordEncoder.encode(user.getPassword()));
userService.create(user);
}
}
UserService
說明:user的service層,不需要多說
package demo.demo1.User.service;
import demo.demo1.User.model.User;
import demo.demo1.User.model.UserRole;
import demo.demo1.User.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User create(User user) throws Exception{
//check that the username already exists
if(user.getUsername() == null) {
throw new Exception("User name can not null");
}
if(userRepository.findByUsername(user.getUsername()) != null) {
throw new Exception(String.format("User %S alrady exist" , user.getUsername()));
}
//check userRole
if(user.getRole() != null) {
if (!user.getRole().equals(UserRole.ROLE_LOWER.getValue()) && !user.getRole().equals(UserRole.ROLE_SENIOR.getValue()) && !user.getRole().equals(UserRole.ROLE_INTERMEDIATE.getValue())) {
throw new Exception(String.format("User Role %s is invalid", user.getRole()));
}
} else {
throw new Exception("User role can not null");
}
return userRepository.save(user);
}
public User getUserById(UUID id) {
return userRepository.findById(id).get();
}
public User getUserByUserName(String name) {
return userRepository.findByUsername(name);
}
public List<User> getAllUser() {
return userRepository.findAll();
}
}
User
說明:user bean
這里的set,get可用@Date注解,但是這里我用的builder模式,在寫代碼的時候service層會報紅,沒有安全感,所以都寫上了
package demo.demo1.User.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.annotations.ApiModelProperty;
import org.hibernate.validator.constraints.NotBlank;
import javax.persistence.*;
import javax.persistence.Column;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.Set;
import java.util.UUID;
@Entity
@Table(name = "DEMO_USER" , indexes = {
@Index(name = "IDX_USER" , columnList = "ID,USERNAME")
})
public class User {
public User(){};
public User(Builder builder) {
setId(builder.id);
setUsername(builder.username);
setPassword(builder.password);
setRole(builder.role);
setEmail(builder.email);
}
@Id
@Column(name = "ID")
@org.hibernate.annotations.Type(type = "org.hibernate.type.PostgresUUIDType")
@ApiModelProperty(value = "用戶ID", required = false, example = "876C2203-7472-44E8-9EB6-13CF372D326C")
private UUID id;
@Column(name = "USERNAME" , length = 60)
@NotBlank(message = "error.not_blank")
@Size(min = 1 , max = 50 , message = "error.size")
@ApiModelProperty(value = "用戶名,長度1~50", required = true, example = "username")
private String username;
@Column(name = "PASSWORD", length = 60)
@NotBlank( message = "error.not_blank")
@Size(min = 1, max = 60 , message = "error.size")
@ApiModelProperty(value = "密碼,長度1~25", required = true, example = "r00tme")
private String password;
@Column(name = "ROLE" , length = 20)
@NotNull
private String role;
@Column(name = "EMAIL" , length = 60)
@Size(min = 1, max = 320, message = "error.size")
@ApiModelProperty(value = "郵箱,長度1~60", required = true, example = "xxx@xxx.com")
private String email;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public static final class Builder {
private UUID id;
private String username;
private String password;
private String role;
private String email;
public Builder setId(UUID val) {
this.id = val;
return this;
}
public Builder setUsername(String val) {
this.username = val;
return this;
}
public Builder setPassword(String val) {
this.password = val;
return this;
}
public Builder setRole(String val) {
this.role = val;
return this;
}
public Builder setEmail(String val) {
this.email = val;
return this;
}
public User build() { return new User(this); }
}
}
UserRepository
package demo.demo1.User.repository;
import demo.demo1.User.model.User;
import demo.demo1.User.model.UserRole;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import java.util.UUID;
@Repository
public interface UserRepository extends JpaRepository<User, UUID>, JpaSpecificationExecutor {
User findByUsername(String username);
}
數(shù)據(jù)流程
spring boot 集成jwt security認證大概流程(轉(zhuǎn)自http://www.reibang.com/p/ca4cebefd1cc)
首先是左邊一張圖,通過登陸接口獲取token儿礼,該接口是任何權(quán)限都能個訪問的
http請求中攜帶username咖杂,password參數(shù)
經(jīng)過security過濾器或者自定義的過濾器(AuthFilter),驗證是否有權(quán)限訪問該接口
在authController中檢查用戶
根據(jù)登錄的用戶生成相應(yīng)的token蚊夫,將token放在response的head中返回
然后是右邊一張圖诉字,說的是如何通過攜帶token訪問接口
在請求的http resquest中加入在登錄是獲取的token參數(shù)
http request經(jīng)過jwtfile驗證,判斷是否是一個合法的token
將token解析出來獲取用戶信息
http request經(jīng)過自定義security authfile過濾
進入資源認證器知纷,判斷是否有權(quán)限訪問請求的接口
代碼github
https://github.com/wheijxiaotbai/HXB_Knowledge/tree/springboot_jwt_security_demo
如何運行
-
postgresql
首先你得準(zhǔn)備一個端口為5433的postgresql,用戶名和密碼為security,db為demo_security
如果你恰巧安裝了docker,請使用以下命令在本地構(gòu)建一個postgresql鏡像,參數(shù)已經(jīng)給出,無需進行其他操作
run -d --name demo_security --restart always -p 5433:5432 -e TZ=Asia/Shanghai -e POSTGRES_USER=security -e POSTGRES_PASSWORD=security -e POSTGRES_DB=demo_security -v /srv/hxb/postgresql/data:/var/lib/postgresql/data postgres:alpine
這里之所以postgresql端口配置的為5433是因為demo中的配置文件指定了5433,你可以通過修改demo中的配置文件進行修改
通過開發(fā)工具打開gradle項目,點擊運行即可
-
在瀏覽器訪問127.0.0.1:8080
在demo中沒有對創(chuàng)建用戶的接口做權(quán)限限制,方便自定義用戶進行測試