Spring Security和JWT结合
先引入pom.xml,主要看dependency部分:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
<relativePath/> <!– lookup parent from repository –>
</parent>
<groupId>com.maxshu</groupId>
<artifactId>test_SpringSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test_SpringSecurity</name>
<description>test_SpringSecurity</description>
<properties>
<java.version>17</java.version>
<kotlin.version>1.8.22</kotlin.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!– Spring Security –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!– Hutool JWT –>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-jwt</artifactId>
<version>5.8.22</version>
</dependency>
<!– GSon, Json –>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
下面是配置文件application.yml:
server:
address: 0.0.0.0
port: 80
logging:
level:
org.springframework.security.web.FilterChainProxy: trace
org.springframework.security.web.access.ExceptionTranslationFilter: trace
org.springframework.security: debug
spring:
application:
name: test_SpringSecurity
profiles:
active: dev
main.banner-mode: ‘off’
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: db
password: db_password
url: jdbc:mysql://127.0.0.1:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
jpa:
database: MYSQL
show-sql: true
generate-ddl: true
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
ddl-auto: update
naming:
implicit-strategy: org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy # Springboot 3.x用
# physical-strategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy # Springboot 2.x用
properties:
hibernate:
format_sql: true
use_sql_comments: true
要建立一个配置类:
@Configuration
@EnableWebSecurity
//@EnableMethodSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //controller启用注解机制的安全,PreAuthorize才会有效
public class WebSecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
@Autowired
private MyAccessDeniedHandler accessDeniedHandler;
@Autowired
private MyUnauthorizedHandler unauthorizedHandler;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
@Bean
public JwtAuthenticationProvider jwtAuthenticationProvider(){
return new JwtAuthenticationProvider();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer ignoringCustomizer() {
//不需要鉴权的访问放这里,在filter之前执行:
return (web) -> web.ignoring()
//如果这些路径没有在Controller中配置,用这些路径来访问的话也会鉴权失败,而且要跟Controller中严格匹配,包括访问时尾部不要”/”。
//允许对于网站静态资源的无授权访问
.requestMatchers(HttpMethod.GET,”/”,”/*.html”,”/*.css”,”/*.js”,”/static/**”)
//对登录注册允许匿名访问
.requestMatchers(“/user/login”,”/user/register”,”/user/logout”)
//跨域请求会先进行一次options请求
.requestMatchers(HttpMethod.OPTIONS)
//测试时全部运行访问.permitAll();
.requestMatchers(“/test/**”);
}
@Bean
public SecurityFilterChain filterChain2(HttpSecurity httpSecurity) throws Exception {
httpSecurity
//由于使用的是JWT,这里不需要csrf防护
.csrf((csrf)->csrf.disable())
//已经经过鉴权filter的访问,再走http过滤:
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated());
//基于token,所以不需要session
httpSecurity.sessionManagement((sm)-> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// 禁用缓存
httpSecurity.headers((header)->header.cacheControl((control)->control.disable()));
//使用自定义provider
httpSecurity.authenticationProvider(jwtAuthenticationProvider());
//添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
httpSecurity.exceptionHandling((handle)->handle
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(unauthorizedHandler));
return httpSecurity.build();
}
}
下面是JwtAuthenticationProvider类:
//登录的时候用来鉴权 public class JwtAuthenticationProvider implements AuthenticationProvider { private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationProvider.class); @Autowired private PasswordEncoder passwordEncoder; @Autowired private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = String.valueOf(authentication.getPrincipal()); String password = String.valueOf(authentication.getCredentials()); UserDetails userDetails = userDetailsService.loadUserByUsername(username); if(passwordEncoder.matches(password,userDetails.getPassword())){ if(userDetails instanceof UserDetailsImpl) { ((UserDetailsImpl)userDetails).setIsLogin(true); } return new UsernamePasswordAuthenticationToken(username, password, userDetails.getAuthorities()); } throw new BadCredentialsException("Password is error!"); } @Override public boolean supports(Class<?> authentication) { // return UsernamePasswordAuthenticationToken.class.equals(authentication); return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
下面是JwtAuthenticationTokenFilter类:
//不能定义为Component、Service等。 //所有访问都会来过滤。 public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); private final static String AUTH_HEADER = "Authorization"; private final static String AUTH_HEADER_TYPE = "Bearer"; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtAuthenticationProvider jwtAuthenticationProvider; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // get token from header: Authorization: Bearer <token> String authHeader = request.getHeader(AUTH_HEADER); if (Objects.isNull(authHeader) || !authHeader.startsWith(AUTH_HEADER_TYPE)){ //不做鉴权处理,所以会直接返回鉴权失败 filterChain.doFilter(request,response); return; } String authToken = authHeader.split(" ")[1]; logger.debug("authToken:{}" , authToken); //verify token,token的提供在AuthController的登录时给的。 if (!JWTUtil.verify(authToken, MyConstant.JWT_SIGNER)) { logger.info("invalid JWT token."); //不做鉴权处理,所以会直接返回鉴权失败 filterChain.doFilter(request,response); return; } final String userName = (String) JWTUtil.parseToken(authToken).getPayload("username"); UserDetails userDetails = userDetailsService.loadUserByUsername(userName); if(request.getRequestURI().equals("/user/logout")){ //退出登录 if(userDetails instanceof UserDetailsImpl) { ((UserDetailsImpl)userDetails).setIsLogin(false); } response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); Message message = new Message<>(true, "退出登录成功"); Gson gson = new Gson(); response.getWriter().println(gson.toJson(message)); response.getWriter().flush(); response.getWriter().close(); return; //不再过滤了。 } if(userDetails instanceof UserDetailsImpl){ if(!((UserDetailsImpl)userDetails).getIsLogin()){ //没有登录,即使带了AUTH字段也不行 //不做鉴权处理,所以会直接返回鉴权失败 filterChain.doFilter(request,response); return; } } //带了token,且已经登录的处理。 // 注意,这里使用的是3个参数的构造方法,此构造方法将认证状态设置为true UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //将认证过了凭证保存到security的上下文中以便于在程序中使用 SecurityContextHolder.getContext().setAuthentication(authentication); filterChain.doFilter(request, response); } }
下面是MyAccessDeniedHandler类:
@Component public class MyAccessDeniedHandler implements AccessDeniedHandler { private static final Logger logger = LoggerFactory.getLogger(MyAccessDeniedHandler.class); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { //logger.error("access error: "+accessDeniedException.getMessage(), accessDeniedException); logger.error("access error: "+accessDeniedException.getMessage()); response.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); Message message = new Message<>(false, "禁止访问"); Gson gson = new Gson(); response.getWriter().println(gson.toJson(message)); response.getWriter().flush(); response.getWriter().close(); } }
下面是MyUnauthorizedHandler类:
@Component
public class MyUnauthorizedHandler implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(MyUnauthorizedHandler.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//logger.error("Unauthorized error: "+authException.getMessage(), authException);
logger.error("Unauthorized error: "+authException.getMessage());
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
Message message = new Message<>(false, "认证失败");
Gson gson = new Gson();
response.getWriter().println(gson.toJson(message));
response.getWriter().flush();
response.getWriter().close();
}
}
下面是MyConstant类:
public class MyConstant { private static final String JWT_SIGN_KEY = "yunbu_key_de$xxSia4R2#@dffDE"; public static final HMacJWTSigner JWT_SIGNER = new HMacJWTSigner( AlgorithmUtil.getAlgorithm("HMD5"), JWT_SIGN_KEY.getBytes(StandardCharsets.UTF_8)); }
本地调试需要,增加一个CORS_Filter类来允许跨域请求:
@Component public class CORS_Filter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest reqs = (HttpServletRequest) req; String curOrigin = reqs.getHeader("Origin"); HttpServletResponse response = (HttpServletResponse) res; response.setHeader("Access-Control-Allow-Origin", curOrigin == null ? "true" : curOrigin); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, PUT, PATCH, GET, OPTIONS, DELETE, HEAD"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Api-Key, Aaccess-Control-Allow-Origin, Authority, Authorization, Content-Type, Accept, Version-Info, X-Requested-With"); // response.setContentType("application/json;charset=UTF-8"); chain.doFilter(req, res); } }
下面为Role和User相关的几个类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private RoleType roleType;
}
public enum RoleType { ADMIN("admin"), USER("user"); private String roleName; RoleType(String roleName) { this.roleName = roleName; } public String getRoleName() { return roleName; } }
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String userName;
private String password;
private static Boolean isLogin = false; //static是临时测试为了保持登录状态,后续挪到数据库后就不能用static了。
public static void setIsLogin(Boolean isLogin){
User.isLogin = isLogin;
}
public static Boolean getIsLogin(){
return User.isLogin;
}
private List<Role> roles;
}
@RequiredArgsConstructor
public class UserDetailsImpl implements UserDetails {
private final User user;
public void setIsLogin(Boolean isLogin){
User.setIsLogin(isLogin);
}
public Boolean getIsLogin(){
return User.getIsLogin();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority(role.getRoleType().getRoleName()))
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = getUserByName(username); if(user == null){ throw new UsernameNotFoundException("User isn't exist!"); } return new UserDetailsImpl(user); } public User getUserByName(String userName) { if (!"007".equals(userName)) { return null; //找不到user } // List<Role> roles = List.of(new Role(RoleType.ADMIN), new Role(RoleType.USER)); // List<Role> roles = List.of( new Role(RoleType.USER)); List<Role> roles = List.of( new Role(RoleType.ADMIN)); return new User(userName, passwordEncoder.encode("123456"), roles); } }
几个和前端打交道的DTO类:
@Data
public class Message<T> {
private Boolean success;
private String msg;
private Page page = null;
private List<T> dataList = null;
public Message(){
;
}
public Message(Boolean success, String msg){
setSuccessAndMsg(success, msg);
}
public void setSuccessAndMsg(Boolean success, String msg){
this.success = success;
this.msg = msg;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Page{
private Integer num;
private Integer size;
private Integer total;
private Long totalRec;
}
}
@Data
public class SignInReq {
private String username;
private String password;
}
@Data @AllArgsConstructor public class JWTAuthToken { String jwtToken; }
最后两个controller类,第一个处理登陆注册:
@RestController @RequestMapping("/user") public class AuthController { private static final Logger logger = LoggerFactory.getLogger(AuthController.class); @Autowired private AuthenticationManager authenticationManager; @PostMapping ("/login") public Message<JWTAuthToken> postLogin(@RequestBody SignInReq req) { logger.info("postLogin, req: {}", req); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword()); authenticationManager.authenticate(authenticationToken); //在config里设置了之后,这里会调用JwtAuthenticationProvider的鉴权 //上一步没有抛出异常说明登录成功,我们向用户颁发jwt令牌,检验在JwtAuthenticationTokenFilter里面。 String token = JWT.create() .setPayload("username", req.getUsername()) .setSigner(MyConstant.JWT_SIGNER) .sign(); //登录成功,返回JWT格式的token给客户端后续再请求时从Header的 JWTAuthToken jwtAuthToken = new JWTAuthToken(token); Message<JWTAuthToken> message = new Message<>(true, ""); message.setDataList(new ArrayList<JWTAuthToken>(Arrays.asList(jwtAuthToken))); return message; } @GetMapping ("/login") public String getLogin() { logger.info("getLogin"); return "getLogin"; } @PostMapping ("/register") public Message<String> postRegister(@RequestBody SignInReq req) { logger.info("postRegister, req: {}", req); //存入数据库。 Message<String> message = new Message<>(true, ""); message.setDataList(new ArrayList<String>(Arrays.asList("注册成功"))); return message; } }
第二个用来处理业务:
@RestController @RequestMapping("/aaa") public class TestController { private static final Logger logger = LoggerFactory.getLogger(TestController.class); @RequestMapping("/aaa") public Message<String> aaa() { logger.info("aaa"); Message<String> message = new Message<>(true, ""); message.setDataList(new ArrayList<String>(Arrays.asList("aaa"))); return message; } @PreAuthorize("hasAuthority('admin')") //需要以什么角色来访问 @GetMapping("/bbb") public Message<String> bbb(){ Message<String> message = new Message<>(true, ""); message.setDataList(new ArrayList<String>(Arrays.asList("bbb"))); return message; } @PreAuthorize("hasAuthority('user')") @GetMapping("/ccc") public Message<String> ccc(){ Message<String> message = new Message<>(true, ""); message.setDataList(new ArrayList<String>(Arrays.asList("ccc"))); return message; } @PreAuthorize("hasAuthority('user') || hasAuthority('admin')") @GetMapping("/ddd") public Message<String> ddd(){ Message<String> message = new Message<>(true, ""); message.setDataList(new ArrayList<String>(Arrays.asList("ddd"))); return message; } }
我们使用curl来模拟注册、登陆、访问、退出登录等操作:
//注册(存数据库时才有用,现在临时测试固定了用户没意义): curl -X POST -H 'Content-Type:application/json' -d '{"username":"007","password":"123456"}' 'http://127.0.0.1/user/register' //模拟登录: curl -X POST -H 'Content-Type:application/json' -d '{"username":"007","password":"123456"}' 'http://127.0.0.1/user/login' //会打印出返回的 token,记住该token。 //模拟登录后的访问:curl -X GET -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0.eyJ1c2VybmFtZSI6IjAwNyJ9.nj2Wp_-CCQGZ44-8uomApw' 'http://127.0.0.1/aaa/aaa' //这里路径/aaa/aaa、/aaa/bbb、/aaa/ccc、/aaa/ddd的处理参考:TestController //模拟退出登录: curl -X GET -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJITUQ1In0.eyJ1c2VybmFtZSI6IjAwNyJ9.nj2Wp_-CCQGZ44-8uomApw' 'http://127.0.0.1/user/logout'