目前主流的认证授权框架包括:Spring Security,Shiro,JWT,Oauth2等。各自都有自己的优缺点和适用场景,百度一下有很多,理论知识了解了,重点还是需要自己上手去实操一篇。
今天和大家分享一个基于springboot整合JWT+Spring Security,实现无状态的认证授权。“无状态“顾名思义,就是不依赖web容器的session会话机制去管理用户的认证信息。
优点也比较明显:
1. 方便实现集群和分布式的认证服务
2. 服务端省去了管理session会话对象的内存和性能开销
3. 便于前后端分离开发模式
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.4.0
com.zkc
springboot-jwt-springsecurity
0.0.1-SNAPSHOT
springboot-jwt-springsecurity
Demo project for Spring Boot
1.8
0.9.0
1.2.55
3.3.1
1.1.21
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
runtime
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
io.jsonwebtoken
jjwt
${jwt.version}
org.springframework.boot
spring-boot-starter-security
com.alibaba
druid-spring-boot-starter
${springboot.druid.starter.version}
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
com.alibaba
fastjson
${fastjson.version}
org.hibernate.validator
hibernate-validator
org.springframework.boot
spring-boot-maven-plugin
# 数据库连接池配置
spring:
datasource:
druid:
url: jdbc:mysql:///jwt_springsecurity?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&serverTimezone=GMT%2B8
db-type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: root
validationQuery: SELECT 1
testWhileIdle: true
#初始化大小,最小,最大
initialSize: 10
minIdle: 10
maxActive: 50
#配置获取连接等待超时的时间
maxWait: 60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
#配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 1800000
#打开PSCache,并且指定每个连接PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
remove-abandoned: true
remove-abandoned-timeout: 1800
log-abandoned: true
# 持久层框架配置
mybatis-plus:
mapper-locations: classpath*:mybatis/**/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jdbc-type-for-null: 'NULL'
map-underscore-to-camel-case: true
global-config:
banner: false
# jwt认证配置
jwt:
expire-interval: 1800
authentication-key: zkcjwt1234bbt
package com.zkc.springbootjwtspringsecurity.config;
import com.zkc.springbootjwtspringsecurity.service.impl.SecurityServiceImpl;
import com.zkc.springbootjwtspringsecurity.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-04
*/
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final SecurityServiceImpl securityService;
private final JwtTokenUtil jwtTokenUtil;
private static final String JWT_PREFIX = "JWTTOKEN ";
private static final String AUTHORIZATION_HEAD = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHead = request.getHeader(AUTHORIZATION_HEAD);
if (authorizationHead != null && authorizationHead.startsWith(JWT_PREFIX)) {
String token = authorizationHead.substring(JWT_PREFIX.length());
String userName = jwtTokenUtil.getUsernameFromToken(token);
// 用户名不等于空 并且未认证过 进行登录验证
if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = securityService.loadUserByUsername(userName);
// 验证token
if (jwtTokenUtil.validateToken(token, userDetails)) {
// 验证通过 构建Secruity登录对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
filterChain.doFilter(request, response);
}
}
package com.zkc.springbootjwtspringsecurity.config;
import com.zkc.springbootjwtspringsecurity.service.impl.SecurityServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-04
*/
@Configuration
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final SecurityServiceImpl securityService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final AuthenticationFailedStrategy authenticationFailedStrategy;
private final AccessFailedStrategy accessFailedStrategy;
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers("/static/**", "/health", "/index.html", "/css/**", "/js/**", "/img/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.addFilterBefore(jwtAuthenticationFilter, AnonymousAuthenticationFilter.class)
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/api/login", "/api/register").permitAll())
.authorizeRequests(authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(authenticationFailedStrategy).accessDeniedHandler(accessFailedStrategy);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(securityService).passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
package com.zkc.springbootjwtspringsecurity.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.zkc.springbootjwtspringsecurity.dto.SecurityUser;
import com.zkc.springbootjwtspringsecurity.dto.UserDO;
import com.zkc.springbootjwtspringsecurity.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
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;
import java.util.ArrayList;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-04
*/
@Service
@RequiredArgsConstructor
public class SecurityServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(UserDO::getUserName, userName);
UserDO userDO = userMapper.selectOne(queryWrapper);
if (userDO != null) {
return new SecurityUser(userName, userDO.getPassWord(), new ArrayList<>(), userDO.getEnabled());
} else {
throw new UsernameNotFoundException(String.format("%s 该账号不存在", userName));
}
}
}
package com.zkc.springbootjwtspringsecurity.config;
import com.zkc.springbootjwtspringsecurity.enums.ResponseCodeEnum;
import com.zkc.springbootjwtspringsecurity.util.ResponseOut;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-09
*/
@Component
public class AuthenticationFailedStrategy implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
ResponseOut.out(response, ResponseCodeEnum.AUTHENTICATION_FAIL.getCode(), e.getMessage());
}
}
package com.zkc.springbootjwtspringsecurity.config;
import com.zkc.springbootjwtspringsecurity.enums.ResponseCodeEnum;
import com.zkc.springbootjwtspringsecurity.util.ResponseOut;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-09
*/
@Component
public class AccessFailedStrategy implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
ResponseOut.out(response, ResponseCodeEnum.ACCESS_FAIL.getCode(), e.getMessage());
}
}
package com.zkc.springbootjwtspringsecurity.service.impl;
import com.zkc.springbootjwtspringsecurity.dto.UserCO;
import com.zkc.springbootjwtspringsecurity.dto.UserDO;
import com.zkc.springbootjwtspringsecurity.enums.UserEnableEnum;
import com.zkc.springbootjwtspringsecurity.mapper.UserMapper;
import com.zkc.springbootjwtspringsecurity.service.LoginRegisterService;
import com.zkc.springbootjwtspringsecurity.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-10
*/
@Service
@RequiredArgsConstructor
public class LoginRegisterServiceImpl implements LoginRegisterService {
private final UserMapper userMapper;
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
@Override
public boolean register(UserCO userCO) {
UserDO userDO = new UserDO();
userDO.setUserName(userCO.getUserName());
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
userDO.setPassWord(passwordEncoder.encode(userCO.getPassWord()));
userDO.setCreateTime(LocalDateTime.now());
userDO.setEnabled(UserEnableEnum.ENABLE.getCode());
return userMapper.insert(userDO) == 1 ? true : false;
}
@Override
public String login(String userName, String passWord) {
// 创建Spring Security登录token
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName, passWord);
// 委托Spring Security认证组件执行认证过程
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 认证成功,设置上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成jwt token
return jwtTokenUtil.generateToken(userName);
}
}
package com.zkc.springbootjwtspringsecurity.controller;
import com.zkc.springbootjwtspringsecurity.dto.ResponseData;
import com.zkc.springbootjwtspringsecurity.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author [email protected]
* @version 1.0.0
* @since 2020-12-10
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class DemoController {
private final UserService userService;
@GetMapping("/userList")
public ResponseData listUser(){
return ResponseData.builderSuccess(userService.listUser());
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/user/{uid}")
public ResponseData getUserById(@PathVariable("uid") Long uid){
return ResponseData.builderSuccess(userService.getUserById(uid));
}
}
码云Git地址:https://gitee.com/zhang_kaicheng/springboot-jwt-springsecurity.git
链接: clone地址