SpringBoot2+SpringSecurity+JWT完成安全认证
1. 写在前面
如何你能看到这边文章,那我觉得我也不需要多废话,文章里面的东西你自然可以看懂。此篇文章只介绍整合过程,不介绍原理,适合懂原理想快速搭建环境的人儿们。
先晒一下项目目录:
2. pom.xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<!-- jwt相关依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.8</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.8</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.8</version>
</dependency>
<!-- spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
</dependencies>
3. application.yml
server:
port: 7100
jwt:
#http请求头
header: Authorization
#token起始标识
start-with: Bearer
#秘钥
secret-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKiuOGSVzac1HH5x3cboD0KapCayatw4G6W59E9Vez6fwix2g5hUFmzRknoZiDREuAXkVi1MqAiQ7Wf8MqqPDvsCAwEAAQ==
#过期时间 单位/秒
validate-second: 43200
4. 创建配置类,映射yml中的变量
/**
* @author : LCheng
* @date : 2020-11-26 16:52
* description : jwt配置参数
*/
@Data
@ConfigurationProperties(prefix = "jwt")
@Configuration
public class JwtProperties {
/**
* http请求头
*/
private String header;
/**
* token起始标识
*/
private String startWith;
/**
* token秘钥
*/
private String secretKey;
/**
* token过期时间 单位/秒
*/
private Long validateSecond;
}
5. 创建AccessDeniedHandler类
SecurityAccessDeniedHandler 用来处理无访问权限的请求,这里只是返回403的操作,可以根据系统来开发相应的功能
/**
* @author : LCheng
* @date : 2020-11-26 16:56
* description : 无访问权限处理类
*/
@Component
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
//根据系统要求开发功能代码
response.sendError(HttpServletResponse.SC_FORBIDDEN, "您的权限不足。");
}
}
6.创建AuthenticationEntryPoint类
SecurityAuthenticationEntryPoint 用来处理认证失败的请求,这里只是返回401的操作,可以根据系统来开发相应的功能
/**
* @author : LCheng
* @date : 2020-11-26 16:58
* description : 认证失败处理类
*/
@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
//根据系统要求开发功能代码
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "认证失败。");
}
}
7.创建jwt工具类
/**
* @author : LCheng
* @date : 2020-11-26 16:59
* description : jwt工具
*/
@Component
@Slf4j
public class JwtUtil {
@Autowired
private JwtProperties jwtProperties;
public static final String USER_KEY = "user";
/**
* 从request中获取token
*
* @param request
* @return {@link String}
* @author LCheng
* @date 2020/11/26 17:15
*/
public String getToken(HttpServletRequest request) {
String token = "";
String bearerToken = request.getHeader(jwtProperties.getHeader());
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtProperties.getStartWith())) {
token = bearerToken.substring(jwtProperties.getStartWith().length());
}
return token;
}
/**
* 根据token获取AuthenticationToken
*
* @param token
* @return {@link UsernamePasswordAuthenticationToken}
* @author LCheng
* @date 2020/11/26 18:16
*/
public UsernamePasswordAuthenticationToken getAuthentication(String token) {
Claims claims = validate(token);
if (claims == null) {
return null;
}
HashMap map = (HashMap) claims.get(USER_KEY);
Collection<? extends GrantedAuthority> authorities =
((List<Map<String, String>>) map.get("authorities")).stream()
.map(a -> new SimpleGrantedAuthority(a.get("authority")))
.collect(Collectors.toList());
User principal = new User((String) map.get("username"), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
/**
* 生成jwt
*
* @param user
* @author LCheng
* @date 2020/11/26 17:15
*/
public String generate(User user) {
String token = Jwts.builder()
.claim(USER_KEY, user)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getValidateSecond() * 1000))
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey()).compact();
return token;
}
/**
* 校验token
*
* @param token
* @return {@link java.lang.Boolean}
* @author LCheng
* @date 2020/11/26 17:16
*/
public Claims validate(String token) {
try {
return Jwts.parser().setSigningKey(jwtProperties.getSecretKey()).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
8.创建SecurityAuthenticationTokenFilter
此过滤器用来拦截请求,并解析token,并将解析的用户信息保存到spring security作用域中
/**
* @author : LCheng
* @date : 2020-11-21 14:36
* description : token验证的过滤器
*/
@Slf4j
@Component
public class SecurityAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.getToken(httpServletRequest);
//判断token是否有效
if (StringUtils.hasText(token)) {
//创建AuthenticationToken
UsernamePasswordAuthenticationToken authentication = jwtUtil.getAuthentication(token);
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} else {
log.debug("token无效。");
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
9.配置项目中用到的bean
/**
* @author : LCheng
* @date : 2020-11-26 17:30
* description : bean
*/
@Component
public class BeanConfig {
@Bean
public FilterRegistrationBean registration(SecurityAuthenticationTokenFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
/**
* 使用BCrypt进行加密
*
* @return {@link PasswordEncoder}
* @author LCheng
* @date 2020/11/26 17:57
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
10.配置UserDetailService
这里是重中之重,所有登录验证功能都是在这里完成的
/**
* 用户登录操作
*
* @author lCheng
*/
@Component
@Slf4j
public class SecurityUserDetailService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String accountName) throws UsernameNotFoundException {
log.info("登陆用户名:" + accountName);
//这里进行用户验证 根据系统自行填写逻辑代码
//因为是测试,这里所有用户密码默认设置为123
return new User(accountName, passwordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
}
}
11.配置SecurityConfig
这里是重中之重中之重,没有他什么也跑不起来,各块代码的注释都已表明
/**
* @author : LCheng
* @date : 2020-11-21 14:43
* description : spring security配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityAccessDeniedHandler securityAccessDeniedHandler;
@Autowired
private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private SecurityAuthenticationTokenFilter securityAuthenticationTokenFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth
// 设置UserDetailsService
.userDetailsService(userDetailsService)
// 使用BCrypt进行加密
.passwordEncoder(passwordEncoder);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config = httpSecurity
// 禁用CSRF
.csrf().disable()
.exceptionHandling()
//认证失败处理
.authenticationEntryPoint(securityAuthenticationEntryPoint)
//无权限处理
.accessDeniedHandler(securityAccessDeniedHandler)
// 不创建session 使用token不需要session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 登录url不验证
.antMatchers("/login").permitAll()
// OPTIONS请求不验证
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll();
// 剩下所有请求都需要认证
config.anyRequest().authenticated();
// 禁用缓存
httpSecurity.headers().cacheControl();
// 添加JWT filter
httpSecurity
.addFilterBefore(securityAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
12.最后一步,配置一个测试控制器
/**
* @author : LCheng
* @date : 2020-11-26 17:44
* description : 测试控制器
*/
@Slf4j
@RestController
public class TestController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@PostMapping(value = "/login")
public String login(@RequestParam String username, @RequestParam String password) {
//验证用户信息
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
SecurityContextHolder.getContext().setAuthentication(authentication);
//生成token
User user = (User) authentication.getPrincipal();
return jwtUtil.generate(user);
}
@GetMapping(value = "/hello")
public String hello() {
return "hello word";
}
}
这里需要注意一下,SecurityConfig中有把 /login 放行,所以要与控制器中进行对应。
13.最最后一步,实践检验真理的最后一步
首先这里要安利一个工具,谷歌浏览器的一个插件: Talend API Tester ,本人喜欢用这个而不是postman。
接下来真是进入测试环节:
- 首先在没有token下,去访问 /hello,由于没有认证会因此返回认证失败。
- 去访问登录接口 /login ,输入错误的密码(密码默认设置为123,在SecurityUserDetailService 有注释请自行查看),由于密码错误因此依然返回认证失败,正常系统会在SecurityUserDetailService 进行密码验证,就可返回自定义的错误信息。
- 去访问登录接口 /login ,输入正确的密码,成功返回了token
- 反正返回的token去再去访问 /hello ,你会发现已经可以正常访问了!
结束语
至此所有整合过程已经完毕。
源码已上传至gitee 地址为:https://gitee.com/lonecheng/springboot-springsecurity-jwt