JWT
请查阅这篇文章介绍:https://blog.csdn.net/qq_33612228/article/details/108853525
Spring Security
Spring Security 是 Spring 全家桶中一个功能强大且高度可定制的身份验证和访问控制框架。与所有 Spring 项目一样,我们可以轻松扩展 Spring Security 以满足自定义要求。由于 Spring Security 功能十分强大,相比于其他技术来说很难上手,很多刚接触 Spring Security 的开发者很难通过文档或者视频就能将其进行运用到实际开发中。 Spring Security 这个强大的安全验证框架来完成用户的登录模块,Spring Security 对于初学者来说,的确很难上手。于是自己在工作之余对这部分知识进行了学习,并实现了一个简单的项目,主要使用了 Spring Boot 技术集成 Spring Security 和 mybatisPlus技术。这个项目实现的比较简单,还有很多地方需要优化,希望有兴趣的朋友可以一起完善。
数据库用户表设计(主要是这四个字段id、user_name、password、role,这个是我自己原先的用户表,多余的在此处未使用)。
CREATE TABLE `sys_user` (
`id` varchar(64) NOT NULL,
`password` varchar(255) NOT NULL DEFAULT '' COMMENT '密码',
`user_name` varchar(50) NOT NULL DEFAULT '' COMMENT '用户的真实姓名',
`role` varchar(255) NOT NULL,
`remarks` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后更新时间',
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_by` varchar(64) NOT NULL DEFAULT '',
`create_by` varchar(64) NOT NULL DEFAULT '',
`del_flag` char(1) NOT NULL DEFAULT '0' COMMENT '0正常,1删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户表';
1、导入springSecurity、jwt、springboot的依赖,pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>sbs</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>sbs</name>
<description>登录和权限</description>
<properties>
<java.version>1.8</java.version>
<mysql-connector-java.version>8.0.11</mysql-connector-java.version>
<druid.version>1.1.14</druid.version>
<mybatis-plus-boot-starter.version>3.1.1</mybatis-plus-boot-starter.version>
<mybatis-plus-generator.version>3.1.1</mybatis-plus-generator.version>
<swagger.version>2.9.2</swagger.version>
<xiaoymin.version>1.9.2</xiaoymin.version>
<fastjson.version>1.2.47</fastjson.version>
<jwt.version>0.9.1</jwt.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<!--阿里巴巴fastjosn依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!--mysql依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MyBatis增强工具-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<!--swagger-api-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger-bootstrap-ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>${xiaoymin.version}</version>
</dependency>
<!-- spring security 安全认证 -->
<!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.10.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.security.oauth.boot/spring-security-oauth2-autoconfigure -->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.4.RELEASE</version>
</dependency>
<!--Token生成与解析-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2、WebCorsConfiguration跨域
WebCorsConfiguration 配置类,主要解决 HTTP 请求跨域问题。这里需要注意的是,如果没有将 Authorization
头字段暴露给客户端的话,客户端是无法获取到 Token 信息的。
package com.example.sbs.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.Collections;
/**
* @Description: 主要解决 HTTP 请求跨域问题。这里需要注意的是,如果没有将 Authorization 头字段暴露给客户端的话,客户端是无法获取到 Token 信息的
* @Author: lst
* @Date 2020-09-27
*/
@Configuration
public class WebCorsConfiguration implements WebMvcConfigurer {
/**
* 设置swagger为默认主页
* 这里可以直接通过url 返回 View
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("redirect:/swagger-ui.html");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
WebMvcConfigurer.super.addViewControllers(registry);
}
/**
* 跨域配置
* @return
*/
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.setAllowedOrigins(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedHeaders(Collections.singletonList("*"));
// 暴露 header 中的其他属性给客户端应用程序
config.setExposedHeaders(Arrays.asList(
"Authorization", "X-Total-Count", "Link",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials"
));
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
3、SecurityConfig配置
SecurityConfig 配置类继承了 Spring Security 的 WebSecurityConfigurerAdapter 类。WebSecurityConfigurerAdapter 类提供了默认的安全配置,并允许其他类通过覆盖其方法来扩展它并自定义安全配置。
这里配置了如下内容:
- 忽略某些不需要验证的就能访问的资源路径;
- 设置
CustomAuthenticationProvider
自定义身份验证组件,用于验证用户的登录信息(用户名和密码); - 在 Spring Security 机制中配置需要验证后才能访问的资源路径、不需要验证就可以访问的资源路径以及指定某些资源只能被特定角色访问。
- 配置请求权限认证异常时的处理类;
- 将自定义的
JwtAuthenticationFilter
和JwtAuthorizationFilter
两个过滤器添加到 Spring Security 机制中。
package com.example.sbs.config;
import com.example.sbs.jwt.JWTAccessDeniedHandler;
import com.example.sbs.jwt.JWTAuthenticationEntryPoint;
import com.example.sbs.jwt.JWTAuthenticationFilter;
import com.example.sbs.jwt.JWTAuthorizationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* @Description
* @author lst
* @date 2020-9-22 12:29
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* @Description TODO 如果将登录接口暴露在 Controller 层,则注释此配置
* @author lst
* @date 2020-9-29 14:15
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
auth.authenticationProvider(authenticationProvider());
}
/**
* 重写hideUserNotFoundExceptions 设置为 false,准确提示用户名还是密码错误
*/
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(bCryptPasswordEncoder());
return provider;
}
/**
* @Description 此方法配置的资源路径不会进入 Spring Security 机制进行验证
* @author lst
* @date 2020-9-29 12:01
* @param web
* @return void
*/
/**@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**")*/
/* .antMatchers("/v2/**")
.antMatchers("/webjars/**")
.antMatchers("/swagger-resources/**")
.antMatchers("/doc.html")
.antMatchers("/swagger-ui.html");
}*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* http.authorizeRequests().antMatchers("/user/**").permitAll();
* 其他都放行了permitAll 无条件拒绝所有denyAll 允许认证过的用户访问-authenticated
*
* .csrf().disable() //禁用CSRF防护
*/
//注册接口放行
http.authorizeRequests().antMatchers("/doc.html").permitAll();
http.authorizeRequests().antMatchers("/swagger-ui.html").permitAll();
http.authorizeRequests().antMatchers("/swagger-resources").permitAll();
http.authorizeRequests().antMatchers("/webjars/**").permitAll();
http.authorizeRequests().antMatchers("/v2/**").permitAll();
http.authorizeRequests().antMatchers("/auth/register").permitAll();
http.authorizeRequests().antMatchers("/user/**").permitAll();
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
// 其他都放行了permitAll 无条件拒绝所有denyAll 允许认证过的用户访问-authenticated
.anyRequest().authenticated()
.and()
// 添加用户登录验证过滤器,将登录请求交给此过滤器处理,如果将登录接口暴露在 Controller 层,则注释这行
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session(不创建会话)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
.accessDeniedHandler(new JWTAccessDeniedHandler()); //添加无权限时的处理
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
4、JwtTokenUtils
主要是对信息进行加密jwt加密生成token,解密、过期时间设置等功能。
package com.example.sbs.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
/**
* @Description jwt
* @author lst
* @date 2020-9-22 12:29
*/
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan";
// 角色的key
private static final String ROLE_CLAIMS = "rol";
// 过期时间是3600秒,既是1个小时
private static final long EXPIRATION = 3600L;
// 选择了记住我之后的过期时间为7天
private static final long EXPIRATION_REMEMBER = 604800L;
// 创建token
public static String createToken(String username,String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
// 从token中获取用户名
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 获取用户角色
public static String getUserRole(String token){
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}
// 是否已过期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
5、UserDetailsServiceImpl、JwtUser
使用springSecurity需要实现UserDetailsService接口供权限框架调用,该方法只需要实现一个方法就可以了,此方法主要是对登录用户和数据库用户进行查询对吧,返回的是UserDetails类型。
package com.example.sbs.jwt;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.sbs.entity.User;
import com.example.sbs.exceptionhandler.BaseException;
import com.example.sbs.exceptionhandler.BaseExceptionEnum;
import com.example.sbs.mapper.UserMapper;
import org.bouncycastle.jcajce.provider.symmetric.util.BaseWrapCipher;
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;
/**
* @Description:
* @Author: lst
* @Date 2020-09-27
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.lambda().eq(User::getUserName,username).last(" limit 1");
User user = userMapper.selectOne(queryWrapper);
if(null == user){
//throw new BaseException(BaseExceptionEnum.USER_NOT_ERROR);
throw new UsernameNotFoundException(BaseExceptionEnum.USER_NOT_ERROR.getMessage());
}
return new JwtUser(user);
}
}
package com.example.sbs.jwt;
import com.example.sbs.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
/**
* @Description:
* @Author: lst
* @Date 2020-09-27
*/
public class JwtUser implements UserDetails {
private String id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;
public JwtUser() {
}
// 写一个能直接使用user创建jwtUser的构造器
public JwtUser(User user) {
id = user.getId();
username = user.getUserName();
password = user.getPassWord();
authorities = Collections.singleton(new SimpleGrantedAuthority("rol"));
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return 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;
}
@Override
public String toString() {
return "JwtUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", authorities=" + authorities +
'}';
}
}
6、JWTAuthenticationFilter
JWTAuthenticationFilter
继承于UsernamePasswordAuthenticationFilter
该拦截器用于获取用户登录的信息,只需创建一个token
并调用authenticationManager.authenticate()
让spring-security去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作,主要配合 CustomAuthenticationProvider
对用户登录请求进行验证,检查登录名和登录密码。如果验证成功,则生成 token 返回。这个操作有点像是shiro的subject.login(new UsernamePasswordToken())
,验证的事情交给框架。
package com.example.sbs.jwt;
import com.alibaba.fastjson.JSON;
import com.example.sbs.exceptionhandler.BaseException;
import com.example.sbs.exceptionhandler.BaseExceptionEnum;
import com.example.sbs.form.LoginUser;
import com.example.sbs.result.BaseResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
/**
* @Description spring-security登录用户进行验证、生成token
* 用于验证使用 URL 地址是 {@link SecurityConstants#AUTH_LOGIN_URL} 进行登录的用户请求。
* 通过检查请求中的用户名和密码参数,并调用 Spring 的身份验证管理器进行验证。
* 如果用户名和密码正确,那么过滤器将创建一个 token,并在 Authorization 标头中将其返回。
* 格式:Authorization: "Bearer + 具体 token 值"</p>
* @author lst
* @date 2020-9-22 12:29
*/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private ThreadLocal<Integer> rememberMe = new ThreadLocal<>();
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/auth/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
try {
ObjectMapper objectMapper= new ObjectMapper();
LoginUser loginUser = objectMapper.readValue(request.getInputStream(), LoginUser.class);
log.info("登录请求的数据:{}",loginUser);
rememberMe.set(loginUser.getRememberMe() == null ? 0 : loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>()));
} catch (Exception e) {
log.error("错误信息:{}",e.getMessage());
response.setHeader("msg",e.getMessage());
throw new BaseException(BaseExceptionEnum.ERROR_SYSTEM.getCode(),e.getMessage());
}
}
// 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
//获取当前登录用户信息 authResult.getPrincipal()
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
boolean isRemember = rememberMe.get() == 1;
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
}
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember);
// String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的时候应该是 `Bearer token`
response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try (PrintWriter out = response.getWriter()) {
String data = JSON.toJSONString(new BaseResponse(HttpServletResponse.SC_OK, "登录成功", null));
out.append(data);
} catch (IOException e1) {
log.error("直接返回Response信息出现IOException异常:" + e1.getMessage());
}
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
7、JWTAuthorizationFilter
验证成功就是进行鉴权了,每一次需要权限的请求都需要检查该用户是否有该权限去操作该资源,当然这也是框架帮我们做的,那么我们需要做什么呢?很简单,只要告诉spring-security该用户是否已登录,是什么角色,拥有什么权限就可以了。用于从用户请求中获取 token 信息,并对其进行验证,同时加载与 token 相关联的用户身份认证信息,并添加到 Spring Security 上下文中。
package com.example.sbs.jwt;
import com.alibaba.fastjson.JSON;
import com.example.sbs.exceptionhandler.BaseException;
import com.example.sbs.result.BaseResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collections;
/**
* @Description 用户请求授权过滤器 token验证
* 提供请求授权功能。用于处理所有 HTTP 请求,并检查是否存在带有正确 token 的 Authorization 标头。
* 如果 token 有效,则过滤器会将身份验证数据添加到 Spring 的安全上下文中,并授权此次请求访问资源。
* @author lst
* @date 2020-9-22 12:29
*/
@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
} catch (Exception e) {
//返回json形式的错误信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String msg = e.getMessage();
try (PrintWriter out = response.getWriter()) {
String data = JSON.toJSONString(new BaseResponse(HttpServletResponse.SC_FORBIDDEN, "无权访问:" + msg, null));
out.append(data);
} catch (IOException e1) {
log.error("直接返回Response信息出现IOException异常:" + e1.getMessage());
}
return;
}
super.doFilterInternal(request, response, chain);
}
/**
* @Description 这里从token中获取用户信息并新建一个token
* @author lst
* @date 2020-9-29 11:02
* @param tokenHeader token
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws Exception {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
boolean expiration = JwtTokenUtils.isExpiration(token);
if (expiration) {
throw new BaseException("token超时了");
} else {
String username = JwtTokenUtils.getUsername(token);
String role = JwtTokenUtils.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
}
return null;
}
}
8、UserController用户注册接口、TaskController测试类
package com.example.sbs.controller;
import com.example.sbs.result.BaseResponse;
import com.example.sbs.result.ResultGenerator;
import com.example.sbs.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* @author LST
* @version 1.0
* @Description: 用户管理
* @since 2020-9-22
*/
@RestController
@Api(value = "UserController", tags = "用户管理")
public class UserController {
@Autowired
private UserService userService;
/**
* @Description 获取用户详情
* @author lst
* @date 2019-12-28 16:36
* @param userId 用户id
* @return User
*/
@GetMapping(value = "/user/{userId}", produces = "application/json; charset=utf-8")
@ApiOperation(value = "获取用户详情", notes = "获取用户详情", code = 200, produces = "application/json")
public BaseResponse getUser(@PathVariable("userId") String userId) {
return ResultGenerator.genSuccessResult(userService.getUser(userId));
}
@GetMapping(value = "/user/list-page", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ApiOperation(value = "用户分页列表", notes = "用户分页列表", produces = "application/json")
@ApiImplicitParams({
@ApiImplicitParam(name = "current", value = "页数", required = false, dataType = "Integer", paramType = "query"),
@ApiImplicitParam(name = "size", value = "条数", required = false, dataType = "Integer", paramType = "query"),
@ApiImplicitParam(name = "name", value = "姓名", required = false,dataType = "string", paramType = "query")
})
public BaseResponse listPage(@RequestParam(name = "current", defaultValue = "1") Integer current, @RequestParam(name = "size", defaultValue = "10") Integer size,
@RequestParam(name = "name", required = false) String name) {
return ResultGenerator.genSuccessResult(userService.userListPage(current, size,name));
}
@PostMapping(value = "/auth/login", produces = "application/json; charset=utf-8")
@ApiOperation(value = "登录", notes = "登录", code = 200, produces = "application/json")
public BaseResponse login(@RequestParam(name = "userName",required = true) String userName,
@RequestParam(name = "passWord",required = true) String passWord) {
return ResultGenerator.genSuccessResult(userService.login(userName,passWord));
}
@PostMapping(value = "/auth/register",produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ApiOperation(value = "注册", notes = "注册", code = 200, produces = "application/json")
public BaseResponse registerUser(@RequestParam(name = "username",required = true) String username,
@RequestParam(name = "password",required = true) String password){
return ResultGenerator.genSuccessResult(userService.registerUser(username,password));
}
}
package com.example.sbs.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* @Description
* @author lst
* @date 2020-9-22 12:29
*/
@RestController
@RequestMapping("/tasks")
public class TaskController {
@GetMapping
public String listTasks(){
return "任务列表";
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public String newTasks(){
return "创建了一个新的任务";
}
@PutMapping("/{taskId}")
public String updateTasks(@PathVariable("taskId")Integer id){
return "更新了一下id为:"+id+"的任务";
}
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId")Integer id){
return "删除了id为:"+id+"的任务";
}
}
9、JWTAccessDeniedHandler没有访问权限、JWTAuthenticationEntryPoint没有携带token或者token无效处理
package com.example.sbs.jwt;
import com.alibaba.fastjson.JSON;
import com.example.sbs.result.BaseResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Description: 没有访问权限 用来解决认证过的用户访问无权限资源时的异常
* @author lst
* @date 2020-9-22 12:29
*/
@Slf4j
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
String msg = "没有访问权限:" + e.getMessage();
log.info("JWTAccessDeniedHandler错误信息:{}",msg);
try (PrintWriter out = response.getWriter()) {
String data = JSON.toJSONString(new BaseResponse(HttpServletResponse.SC_FORBIDDEN, "无权访问:" + msg, null));
out.append(data);
} catch (IOException e1) {
log.error("直接返回Response信息出现IOException异常:" + e1.getMessage());
}
}
}
package com.example.sbs.jwt;
import com.alibaba.fastjson.JSON;
import com.example.sbs.result.BaseResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @Description 没有携带token或者token无效 用来解决匿名用户访问无权限资源时的异常
* @author lst
* @date 2020-9-22 12:29
*/
@Slf4j
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) {
String msg = response.getHeader("msg");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
if(StringUtils.isEmpty(msg)){
msg = "没有携带token或者token无效:" + e.getMessage();
}else if("Bad credentials".contains(msg)){
msg = "密码错误";
}
log.info("JWTAuthenticationEntryPoint错误信息:{}",msg);
try (PrintWriter out = response.getWriter()) {
String data = JSON.toJSONString(new BaseResponse(HttpServletResponse.SC_FORBIDDEN, "无权访问:" + msg, null));
out.append(data);
} catch (IOException e1) {
log.error("直接返回Response信息出现IOException异常:" + e1.getMessage());
}
}
}
10、测试
1)注册登录账号
2)登录
登录成功
3)验证token,请求其他接口
不带token请求
带不正确的token(这里不带token和错误的token我没有正确抛出对应的异常)
带正确的token
源码Git地址:https://gitee.com/liangshitian/sbs