之前一直都是用shiro在做用户的验证和鉴权,最近在SpringSecurity上也踩了不少坑。对于SpringSecurity比较官方的解释是:Spring Security是Spring提供的一个安全框架,提供认证和授权功能,最主要的是它提供了简单的使用方式,同时又有很高的灵活性,简单,灵活,强大。但在使用的时候,却没有说的那样轻松,明明作为一个框架,应该是又简单又方便使用,配合上JWT,那就是双倍的快乐。
SpringBoot、SpringSecurity、JWT、Spring Data JPA
创建maven项目,导入所需依赖
4.0.0
com.chen
SpringSecurity_jwt_test
0.0.1-SNAPSHOT
jar
SpringSecurity_jwt_test
org.springframework.boot
spring-boot-starter-parent
2.0.3.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
io.jsonwebtoken
jjwt
0.9.0
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
1.18.12
provided
org.springframework.boot
spring-boot-maven-plugin
server.port=8028
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
关于JPA这里就不进行解释。需要的可以查看JPA的使用文章:
Spring Data JPA -- 环境搭建
Spring Data JPA -- 单表操作
Spring Data JPA -- 多表操作(一对一)
Spring Data JPA -- 多表操作(一对多)
新建一个用户实体,属性包括账号、密码、角色,这里因为使用了JPA框架,所以当项目启动时,会将该实体类自动生成对应的数据库表。
package com.chen.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
/**
* author:chen
*/
@Entity
@Getter
@Setter
@Table(name = "user_test")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "role")
private String role;
}
对jwt进行封装,以便后面可以直接调用。
package com.chen.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
/**
* author:chen
*/
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "mysecret";
private static final String ISS = "chen";
// 角色的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 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();
}
}
这里我只定义了一个方法(通过用户名查找用户),其他常见的单表操作方法JPA已经在内部帮我们实现了,这就是JPA的快乐。
package com.chen.repository;
import com.chen.entity.User;
import org.springframework.data.repository.CrudRepository;
/**
* author:chen
*/
public interface UserRepository extends CrudRepository {
User findByUsername(String username);
}
使用SpringSecurity需要实现UserDetailsService接口供权限框架调用,该方法只需要实现一个方法就可以了,那就是根据用户名去获取用户,那就是上面repository定义的方法了,这里直接调用了。
package com.chen.service;
import com.chen.entity.JwtUser;
import com.chen.entity.User;
import com.chen.repository.UserRepository;
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;
/**
* author:chen
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
return new JwtUser(user);
}
}
由于接口方法需要返回一个UserDetails类型的接口,所以这边就再写一个类去实现一下这个接口。
package com.chen.entity;
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;
/**
* author:chen
*/
public class JwtUser implements UserDetails {
private Integer 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(user.getRole()));
}
@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 +
'}';
}
}
JWTAuthenticationFilter继承于UsernamePasswordAuthenticationFilter 该拦截器用于获取用户登录的信息,只需创建一个token并调用authenticationManager.authenticate()让SpringSecurity去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给SpringSecurity去操作。 这个操作有点像是shiro的subject.login(new UsernamePasswordToken()),验证的事情交给框架。
package com.chen.filter;
import com.chen.model.LoginUser;
import com.chen.utils.JwtTokenUtils;
import com.chen.entity.JwtUser;
import com.fasterxml.jackson.databind.ObjectMapper;
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.util.ArrayList;
import java.util.Collection;
/**
* author:chen
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private ThreadLocal rememberMe = new ThreadLocal<>();
private AuthenticationManager authenticationManager;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 前端传过来的是json数据
/*try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
rememberMe.set(loginUser.getRememberMe() == null ? 0 : loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (Exception e) {
e.printStackTrace();
return null;
}*/
// 前端传过来的是key-value数据
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("username:"+username+",password:"+password);
LoginUser loginUser=new LoginUser(username,password);
rememberMe.set(loginUser.getRememberMe() == null ? 0 : loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>()));
}
// 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
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("Authorization", JwtTokenUtils.TOKEN_PREFIX + token);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
花了一点时间在扯这部分的源码,果然框架就是框架,连账户名参数,密码参数,登录接口都给你设定好了,当然,我们可以进行重写,以上过滤器是在自己理解下进行改造重写。
验证成功当然就是进行鉴权了,每一次需要权限的请求都需要检查该用户是否有该权限去操作该资源,当然这也是框架帮我们做的,那么我们需要做什么呢?只要告诉SpringSecurity该用户是否已登录,是什么角色,拥有什么权限就可以了。 JWTAuthenticationFilter继承于BasicAuthenticationFilter。
package com.chen.filter;
import com.chen.utils.JwtTokenUtils;
import com.chen.exception.TokenIsExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.util.Collections;
/**
* author:chen
*/
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 (TokenIsExpiredException e) {
//返回json形式的错误信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "统一处理,原因:" + e.getMessage();
response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
response.getWriter().flush();
return;
}
super.doFilterInternal(request, response, chain);
}
// 这里从token中获取用户信息并新建一个token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws TokenIsExpiredException {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
boolean expiration = JwtTokenUtils.isExpiration(token);
if (expiration) {
throw new TokenIsExpiredException("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;
}
}
package com.chen.config;
import com.chen.exception.JWTAccessDeniedHandler;
import com.chen.exception.JWTAuthenticationEntryPoint;
import com.chen.filter.JWTAuthenticationFilter;
import com.chen.filter.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.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.NoOpPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* author:chen
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 对密码进行编码
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
// 对密码不进行处理
//auth.userDetailsService(userDetailsService).passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
.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;
}
}
package com.chen.model;
import lombok.Getter;
import lombok.Setter;
/**
* author:chen
*/
@Setter
@Getter
public class LoginUser {
private String username;
private String password;
private Integer rememberMe;
}
package com.chen.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* author:chen
*/
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "统一处理,原因:" + e.getMessage();
httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(reason));
}
}
package com.chen.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* author:chen
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "统一处理,原因:" + authException.getMessage();
response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
}
}
package com.chen.exception;
/**
* author:chen
*/
public class TokenIsExpiredException extends Exception {
public TokenIsExpiredException() {
}
public TokenIsExpiredException(String message) {
super(message);
}
public TokenIsExpiredException(String message, Throwable cause) {
super(message, cause);
}
public TokenIsExpiredException(Throwable cause) {
super(cause);
}
public TokenIsExpiredException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
这是出于个人习惯封装的返回数据处理类。
package com.chen.model;
public class AppConstant {
// 文本消息
public static final String MESSAGE = "message";
// 单个对象
public static final String ITEM = "item";
// 返回的对象列表
public static final String LIST = "list";
// 状态码
public static final String ERROR = "error";
// 代表执行成功
public static int OK = 0;
// 代表执行失败
public static int FAIL = 1;
}
package com.chen.utils;
import com.chen.model.AppConstant;
import java.util.HashMap;
import java.util.List;
public class RestResponse extends HashMap {
/**
* 禁止通过构造函数构造对象,只能通过静态方法获取实例。
*
* @see #ok()
* @see #ok(String)
* @see #fail()
* @see #fail(String)
*/
private RestResponse() {
}
/**
* 设置接口返回的文本消息,属性 key: message
*
* @param msg
* @return
*/
public RestResponse msg(String msg) {
this.put(AppConstant.MESSAGE, msg);
return this;
}
/**
* 设置接口返回的数据对象,属性 key: item
*
* @param item
* @return
*/
public RestResponse item(Object item) {
this.put(AppConstant.ITEM, item);
return this;
}
/**
* 设置接口返回的数据对象列表,属性 key: list
*
* @param list
* @return
*/
public RestResponse list(List> list) {
this.put(AppConstant.LIST, list);
return this;
}
/**
* 设置接口返回的数据项,并指定数据项的属性 key
*
* @param key
* @param value
* @return
*/
public RestResponse put(String key, Object value) {
super.put(key, value);
return this;
}
/**
* 接口执行成功的返回数据,其中属性 error = 0
*
* @return
*/
public static RestResponse ok() {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, AppConstant.OK);
return result;
}
/**
* 接口执行成功的返回数据,并设置文本消息
*
* @param msg
* @return
*/
public static RestResponse ok(String msg) {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, AppConstant.OK).msg(msg);
return result;
}
/**
* 接口执行成功的返回数据,并设置对象数据
*
* @param item
* @return
*/
public static RestResponse ok(Object item) {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, AppConstant.OK).item(item);
return result;
}
/**
* 接口执行成功的返回数据,并设置列表对象数据
*
* @param list
* @return
*/
public static RestResponse ok(List> list) {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, AppConstant.OK).list(list);
return result;
}
/**
* 接口执行失败的返回数据,其中属性 error = 1
*
* @return
*/
public static RestResponse fail() {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, AppConstant.FAIL);
return result;
}
/**
* 接口执行失败的返回数据,并设置文本消息,其中属性 error = 1, message = {msg}
*
* @param msg
* @return
*/
public static RestResponse fail(String msg) {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, AppConstant.FAIL).msg(msg);
return result;
}
/**
* 接口执行失败的返回数据,自定义状态码,其中属性 error = {errcode}
*
* @param errcode
* @return
*/
public static RestResponse fail(int errcode) {
RestResponse result = new RestResponse();
result.put(AppConstant.ERROR, errcode);
return result;
}
}
到此整合完毕,接下来创建一个接口进行用户的注册。
package com.chen.controller;
import com.chen.entity.User;
import com.chen.repository.UserRepository;
import com.chen.utils.RestResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
/**
* author:chen
*/
@RestController
public class AuthController {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/register")
public RestResponse registerUser(String username, String password){
User user = new User();
user.setUsername(username);
//对密码进行编码
user.setPassword(bCryptPasswordEncoder.encode(password));
//不对密码进行编码,存储明文
//user.setPassword(password);
user.setRole("ROLE_USER");
User save = userRepository.save(user);
return RestResponse.ok().item(save);
}
}
注册成功,并且设置了该用户的角色为ROLE_USER。接下来使用这个角色进行登录,登录成功之后在返回的响应头可以得到我们想要的token。
登录的接口我们已经在JWTAuthenticationFilter过滤器设置了。
利用在响应头返回的token,便可以进行身份鉴权了。
附上我的代码仓库:https://github.com/ws980728/SpringSecurity_jwt_test/tree/master