2021SC@SDUSC
Spring Security+JWT实现鉴权
零. spring security鉴权流程简图
一. 用gradle/maven导入依赖(gradle)
首先导入以下依赖
// spring security
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.5.5'
// jjwt
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
// json
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
二. 建立配置文件
整体配置文件
首先建立全局的Spring security的配置文件
// 开启spring security
@EnableWebSecurity
// 开启pre/post注解拦截
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//禁用session与csrf,这里打算采用token的方式所以不用session
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).disable();
// 禁用form表单登录
http.formLogin().disable();
// 配置允许跨域
http.cors();
// 允许不经验证通过的接口
http.authorizeRequests().antMatchers("/login", "/register").permitAll()
// 其余接口均需要验证
.anyRequest().authenticated();
// option请求拦截器,cors请求如果是application/json之类的会首先做一次Options请求
http.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
.apply(new UsernameLoginConfig<>()) // 应用用户名密码登录的config
.and()
.apply(new JwtLoginConfig<>()) //应用jwt登录的config
.and().logout().disable(); //禁用默认的退出
}
/**
* 配置cors跨域访问
* @return cors配置
*/
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
// 暴露请求头,这里的authentication头是jwt的规范之一
configuration.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
/**
* 密码编码器
* @return BCryptPasswordEncoder
*/
@Bean
PasswordEncoder getPw() {
return new BCryptPasswordEncoder();
}
}
Options请求拦截器
配置Options请求拦截器OptionsRequestFilter
public class OptionsRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (request.getMethod().equals("OPTIONS")) {
// 允许通过的请求,这里仅允许get/post/head如果需要可以添加put/delete等等
response.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
response.setHeader("Access-Control-Allow-Headers", response.getHeader("Access-Control-Request-Headers"));
return;
}
filterChain.doFilter(request, response);
}
}
配置用户名登录拦截
spring security实际上有自己自带的UsernamePasswordAuthenticationFilter,我们在这儿就直接采用他的
Config&Filter
首先是配置UsernameLoginConfig.java配置文件
/**
* 用户名登录的配置
*/
@Configuration
public class UsernameLoginConfig<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<T, B> {
// 使用spring security自带的用户名密码校验
// 默认的拦截/login的post请求,取出username password做校验
private final UsernamePasswordAuthenticationFilter authFilter = new UsernamePasswordAuthenticationFilter();
@Override
public void configure(B builder) {
authFilter.setPostOnly(true);
// 获取系统默认的manager
authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
// 不使用session
authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
// 利用bean注入handler,可以交给spring控制生命周期,不用多次实例化
authFilter.setAuthenticationFailureHandler(loginFailureHandler());
authFilter.setAuthenticationSuccessHandler(usernameLoginSuccessHandler());
UsernamePasswordAuthenticationFilter filter = postProcess(authFilter);
//指定Filter的位置
builder.addFilterAfter(filter, LogoutFilter.class);
}
@Bean
LoginFailureHandler loginFailureHandler(){
return new LoginFailureHandler();
}
@Bean
UsernameLoginSuccessHandler usernameLoginSuccessHandler(){
return new UsernameLoginSuccessHandler();
}
}
handler
接着是配置UsernameLoginSuccessHandler,这个是用来处理登录成功后的流程,我们要做的是JWT的鉴权,因此在这个handler里应当生成jwt并给前端返回
public class UsernameLoginSuccessHandler implements AuthenticationSuccessHandler {
private final static Gson gson = new Gson();
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException{
//生成token,并放置在header里
String token = JwtUtils.createToken(((BallUser)authentication.getPrincipal()).getUserId());
Map.Entry<String, String> entry = JwtUtils.tokenToEntry(token);
response.setHeader(entry.getKey(), entry.getValue());
response.setCharacterEncoding("UTF-8");
response.getWriter().print(gson.toJson(ResultEntity.data(token)));
}
}
JwtUtil
这里放出来一个略微封装的jwt生成工具,可以根据项目自行定制
public class JwtUtils {
public static final String TOKEN_HEADER = "Authorization"; //Header标识JWT
private static final String TOKEN_PREFIX = "Bearer "; //JWT标准开头,注意空格
private static final String SECRET = "jwt-key"; //JWT签证密钥
private static final String ROLE = "role"; //Jwt中携带的身份key
private static final String USER_ID = "uid"; //Jwt中携带的用户ID的key
private static final long EXPIRATION = 60 * 60 * 24 * 7L; //过期时间7天
/**
* 从http header中map中检索token
*
* @param headers http的header
* @return 如果检索到返回token否则返回null
*/
public static String tokenFromHeaders(Map<String, String> headers) {
if (headers == null) {
return null;
}
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().equals(TOKEN_HEADER)) {
if (entry.getValue().startsWith(TOKEN_PREFIX)) {
return entry.getValue().replaceFirst(TOKEN_PREFIX, "");
}
}
}
return null;
}
public static String tokenFromHeader(String header) {
if (header == null) {
return null;
}
if (header.startsWith(TOKEN_PREFIX)) {
return header.replaceFirst(TOKEN_PREFIX, "");
}
return null;
}
/**
* 将token变成jwt标准的entry
*
* @param token token不能为null
* @return 转换成的entry
*/
public static Map.Entry<String, String> tokenToEntry(String token) {
assert token != null;
return new AbstractMap.SimpleEntry<String, String>(TOKEN_HEADER, TOKEN_PREFIX + token);
}
/**
* 创建JWT
*
* @param userId 用户ID
* @return 创建好的Token
*/
public static String createToken(Integer userId) {
Map<String, Object> map = new HashMap<>();
map.put(USER_ID, userId);
return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setSubject("info")
.setIssuer("weiteli")
.setAudience("together-ball")
.setClaims(map).setIssuedAt(new Date())
// token 7天内有效
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
}
/**
* 重新刷新token有效期
*
* @param token 需要刷新的token
* @return 已经刷新的token
*/
public static String refreshToken(String token) {
Claims claim = getTokenClaims(token);
return Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setClaims(claim).setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
}
/**
* 根据token获取用户ID
*
* @param token JWT
* @return 用户名
*/
public static String getUsername(String token) {
try {
return getTokenClaims(token).getSubject();
} catch (Exception e) {
return null;
}
}
/**
* 获取Token载体信息
*
* @param token JWT
* @return token携带的claim
*/
private static Claims getTokenClaims(String token) {
Claims claims = null;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
e.printStackTrace();
}
return claims;
}
/**
* 获取token携带的用户角色列表
*
* @param token JWT
* @return 用户角色, 以英文逗号(, )分隔开
*/
public static String getUserRole(String token) {
return (String) getTokenClaims(token).get(ROLE);
}
/**
* 获取token携带的用户ID
*
* @param token JWT
* @return 用户ID
*/
public static int getUserId(String token) {
return getTokenClaims(token).get(USER_ID, Integer.class);
}
/**
* 判断token是否过期
*
* @param token JWT
* @return 是否过期
*/
public static Boolean isExpiration(String token) {
return getTokenClaims(token).getExpiration().before(new Date());
}
/**
* 获取签发日期
*
* @param token JWT
* @return 签发Token的日期
*/
public static Date getIssuedAt(String token) {
return getTokenClaims(token).getIssuedAt();
}
}
接着是配置失败的统一处理,这个也很简单,默认是失败返回403,这里根据需求对其定制
ResultEntity以及StatusCode均是自定的类,输出string也是一样的
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException {
Gson gson = new Gson();
response.setCharacterEncoding("UTF-8");
response.getWriter().print(gson.toJson(ResultEntity.error(StatusCode.USER_CREDENTIALS_ERROR)));
response.setStatus(HttpStatus.OK.value());
}
}
UserDetailService
UsernamePasswordAuthenticationFilter会调用UserDetailService的loadUsername方法,一般需要在系统中重写service
首先定义鉴权的类,这个系统中通过userId唯一区分用户,因此需要重写
如果通过用户名唯一区分,直接使用org.springframework.security.core.userdetails.User可以不用重写
public class BallUser extends org.springframework.security.core.userdetails.User {
private final int userId;
public BallUser(int userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
this.userId = userId;
}
public BallUser(int userId, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
this.userId = userId;
}
public int getUserId() {
return userId;
}
}
接着重写service
@Service
public class UserService implements UserDetailsService {
@Autowired
UserDao userDao;
@Autowired
RoleDao roleDao;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.selectUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户名不存在");
}
List<String> roles = roleDao.selectRolesByUserId(user.getUserId());
roles.add(RoleEnum.COMMON.getRoleName());
for (int i = 0; i < roles.size(); i++) {
roles.set(i, "ROLE_" + roles.get(i));
}
String roleString = StringUtils.arrayToCommaDelimitedString(roles.toArray());
return new BallUser(
user.getUserId(),
user.getUsername(),
user.getPassword(),
AuthorityUtils.commaSeparatedStringToAuthorityList(roleString));
}
public Boolean register(String username, String password) {
if (userDao.selectUserByUsername(username) == null) {
return userDao.insertUser(username, passwordEncoder.encode(password));
}
return false;
}
public List<String> loadRolesByUserId(int userId){
List<String> roles = roleDao.selectRolesByUserId(userId);
roles.add(RoleEnum.COMMON.getRoleName());
for (int i = 0; i < roles.size(); i++) {
roles.set(i, "ROLE_" + roles.get(i));
}
return roles;
}
}
配置Token拦截
config
首先还是配置config
/**
* Token登录的配置
*/
@Configuration
public class JwtLoginConfig<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
extends AbstractHttpConfigurer<T, B> {
@Override
public void configure(B builder) throws Exception {
builder.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() {
// 使用自定义的filter
JwtAuthenticationFilter filter = new JwtAuthenticationFilter();
// 新建providerManager并为filter配置provider
ProviderManager providerManager = new ProviderManager(new JwtAuthenticationProvider());
filter.setAuthenticationManager(providerManager);
// 不使用session
filter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());
// 通过bean注入handler
filter.setAuthenticationSuccessHandler(jwtLoginSuccessHandler());
filter.setAuthenticationFailureHandler(new LoginFailureHandler());
return filter;
}
@Bean
JwtLoginSuccessHandler jwtLoginSuccessHandler(){
return new JwtLoginSuccessHandler();
}
}
Filter
接着自定义filter,这里的jwt拦截需要我们自定义拦截方式
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public JwtAuthenticationFilter() {
// 拦截请求头中带有Authentication的
super(new RequestHeaderRequestMatcher(JwtUtils.TOKEN_HEADER));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setHeader("Content-Type", "text/html;charset=UTF-8");
String token = getJwtToken(request);
JwtAuthenticationToken authRequest = new JwtAuthenticationToken(null, token, null);
return getAuthenticationManager().authenticate(authRequest);
}
// 这里一定要注意,必须重写,否则controller收不到请求
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
chain.doFilter(request, response);
}
private String getJwtToken(HttpServletRequest request) {
String header = request.getHeader(JwtUtils.TOKEN_HEADER);
return JwtUtils.tokenFromHeader(header);
}
}
Token(鉴权信息容器)
自定义Token用以保存鉴权的信息
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
// 保持与UsernameAuthentication的一致性
private final BallUser principal;
private final Object credentials;
public JwtAuthenticationToken(BallUser user, String token, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = user;
this.credentials = token;
}
@Override
public BallUser getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return credentials;
}
}
Provider
接着自定义验证的Provider
// @Component 这个component也是不需要的
public class JwtAuthenticationProvider implements AuthenticationProvider {
// 这里注意,由于前面的实例都是new出来的,没有走spring的bean管理,这儿是没办法直接注入的
// 需要获取bean
// @Autowired
// RoleDao roleDao;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtAuthenticationToken jwtToken = (JwtAuthenticationToken) authentication;
String token = (String) jwtToken.getCredentials();
try {
int userId = JwtUtils.getUserId(token);
// 获取service bean
UserService userService = GetBeanUtils.getBean(UserService.class);
List<String> roles = userService.loadRolesByUserId(userId);
BallUser user = new BallUser(
userId, "[NotNeed]", "[Protected]",
AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(roles.toArray())));
return new JwtAuthenticationToken(user, token, user.getAuthorities());
} catch (JwtException e) {
e.printStackTrace();
throw new AuthenticationServiceException("Token invalidate");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
Spring Security是通过鉴权过程中的异常来判断是否鉴权成功的,如果中途任何地方抛出AuthenticationException异常,即认为鉴权失败,没有任何异常返回结果认为鉴权成功
附录:
GetBeanUtils的内容参见 这篇博客
GetBeanUtils内容如下
// [这篇博客](https://blog.csdn.net/qq_28080659/article/details/99687074)
@Component
public class GetBeanUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (GetBeanUtils.applicationContext == null) {
GetBeanUtils.applicationContext = applicationContext;
}
}
/**
* 返回ApplicationContext
*
* @return ApplicationContext
*/
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 返回bean
*
* @param beanName beanName
* @return bean
*/
public static Object getBean(String beanName) {
return applicationContext.getBean(beanName);
}
/**
* 获取bean
*
* @param c c
* @param <T> 泛型
* @return bean
*/
public static <T> T getBean(Class<T> c) {
return applicationContext.getBean(c);
}
/**
* 获取bean
* @param c c
* @param name 名称
* @param <T> 泛型
* @return T 泛型
*/
public static <T> T getBean(String name, Class<T> c) {
return getApplicationContext().getBean(name, c);
}
}
三. Controller层验证
controller代码如下
@RestController
@RequestMapping("/")
public class UserController {
@Autowired
UserService userService;
@RequestMapping("register")
ResultEntity register(@RequestParam String username, @RequestParam String password) {
if (userService.register(username, password)) {
return ResultEntity.success();
} else {
return ResultEntity.error(StatusCode.USER_ALREADY_EXISTING);
}
}
@RequestMapping("logout")
@PreAuthorize("hasRole('COMMON')")
// @PreAuthorize("hasRole('ROLE_COMMON')") 这么写与上一行等价,ROLE_可省
ResultEntity logout() {
return ResultEntity.success();
}
}
-
不需要权限的接口 /register直接访问
-
需要权限的接口 /logout需要权限
在有权限的情况下
略微修改上面的controller代码,再次请求接口
@RequestMapping("logout") @PreAuthorize("hasRole('SYSTEM')") ResultEntity logout() { return ResultEntity.success(); }
这里会显示没有权限返回403
-
隐含的拦截器接口 /login
一些注意事项
- 用到autowired的类,除了必须要有@Component/@Service等注解意外,必须要保证,其声明周期由springboot管理,而不是直接new出来的
- jwt要求的规范一定要注意,header是
Authoritarian
内容是bearer
紧跟token,特别注意bearer后面有个space空格 - 重写AbstractAuthenticationProcessingFilter的时候,必须重写successfulAuthentication方法,否则后面的请求链会收不到请求,重写的时候需要添加
chain.doFilter(request, response);
- 如果在new出来的类中想要使用spring的bean,可以参照配置token拦截的最后一部分使用GetBeanUtil