文章目录
一. 登录校验
1. 引入 security jwt 插件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
2. SecurityConfig 配置
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private TokenFilter tokenFilter;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
// 基于token,所以不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//这些url不拦截
http.authorizeRequests()
.antMatchers("/", "/*.html", "/favicon.ico", "/css/**", "/js/**", "/fonts/**", "/layui/**", "/img/**",
"/v2/api-docs/**", "/swagger-resources/**", "/webjars/**", "/pages/**", "/druid/**",
"/statics/**")
.permitAll().anyRequest().authenticated();
//对于/login请求,设置成功失败的handler
http.formLogin().loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler).failureHandler(authenticationFailureHandler).and()
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint);
http.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 解决不允许显示在iframe的问题
http.headers().frameOptions().disable();
http.headers().cacheControl();
//先进行UsernamePasswordAuthenticationFilter校验,再进行tokenFilter校验
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
}
3. 重写登录成功失败处理方法
@Configuration
public class SecurityHandlerConfig {
@Autowired
private TokenService tokenService;
/**
* 登陆成功,返回Token
*
* @return
*/
@Bean
public AuthenticationSuccessHandler loginSuccessHandler() {
return new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//执行UsernamePasswordAuthenticationFilter 已经将user信息存储至authentication中
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
//这里是第一次将user信息存储至redis的地方,并生成token
Token token = tokenService.saveToken(loginUser);
ResponseUtil.responseJson(response, HttpStatus.OK.value(), token);
}
};
}
/**
* 登陆失败
*
* @return
*/
@Bean
public AuthenticationFailureHandler loginFailureHandler() {
return new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String msg = null;
if (exception instanceof BadCredentialsException) {
msg = "密码错误";
} else {
msg = exception.getMessage();
}
ResponseInfo info = new ResponseInfo(HttpStatus.UNAUTHORIZED.value() + "", msg);
ResponseUtil.responseJson(response, HttpStatus.UNAUTHORIZED.value(), info);
}
};
}
}
4. token 生成及存储
//这个注解保证了有多个TokenService的实现类时,首先注入本类
@Primary
@Service
public class TokenServiceJWTImpl implements TokenService {
private static final Logger log = LoggerFactory.getLogger("adminLogger");
/**
* token过期秒数
*/
@Value("${token.expire.seconds}")
private Integer expireSeconds;
@Autowired
private RedisTemplate<String, LoginUser> redisTemplate;
@Autowired
private SysLogService logService;
/**
* 私钥
*/
@Value("${token.jwtSecret}")
private String jwtSecret;
private static Key KEY = null;
private static final String LOGIN_USER_KEY = "LOGIN_USER_KEY";
@Override
public Token saveToken(LoginUser loginUser) {
//这里loginUser中存储的token只是一串uuid
loginUser.setToken(UUID.randomUUID().toString());
cacheLoginUser(loginUser);
// 登陆日志
logService.save(loginUser.getId(), "登陆", true, null);
String jwtToken = createJWTToken(loginUser);
return new Token(jwtToken, loginUser.getLoginTime());
}
/**
* 生成jwt
*
* @param loginUser
* @return
*/
private String createJWTToken(LoginUser loginUser) {
Map<String, Object> claims = new HashMap<>();
claims.put(LOGIN_USER_KEY, loginUser.getToken());// 放入一个随机字符串,通过该串可找到登陆用户
String jwtToken = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, getKeyInstance())
.compact();
return jwtToken;
}
private void cacheLoginUser(LoginUser loginUser) {
loginUser.setLoginTime(System.currentTimeMillis());
// 自己控制到期时间,而不是通过Jwt,可以做到不断刷新过期时间
loginUser.setExpireTime(loginUser.getLoginTime() + expireSeconds * 1000);
// 根据uuid将loginUser缓存,loginUser在redis中也设置了过期时间
redisTemplate.boundValueOps(getTokenKey(loginUser.getToken())).set(loginUser, expireSeconds, TimeUnit.SECONDS);
}
/**
* 更新缓存的用户信息
*/
@Override
public void refresh(LoginUser loginUser) {
cacheLoginUser(loginUser);
}
@Override
public LoginUser getLoginUser(String jwtToken) {
//先从jwttoken中解析出uuid,再通过uuid从redis中取出user信息
String uuid = getUUIDFromJWT(jwtToken);
if (uuid != null) {
return redisTemplate.boundValueOps(getTokenKey(uuid)).get();
}
return null;
}
@Override
public boolean deleteToken(String jwtToken) {
String uuid = getUUIDFromJWT(jwtToken);
if (uuid != null) {
String key = getTokenKey(uuid);
LoginUser loginUser = redisTemplate.opsForValue().get(key);
if (loginUser != null) {
redisTemplate.delete(key);
// 退出日志
logService.save(loginUser.getId(), "退出", true, null);
return true;
}
}
return false;
}
private String getTokenKey(String uuid) {
return "tokens:" + uuid;
}
private Key getKeyInstance() {
if (KEY == null) {
synchronized (TokenServiceJWTImpl.class) {
if (KEY == null) {// 双重锁
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(jwtSecret);
KEY = new SecretKeySpec(apiKeySecretBytes, SignatureAlgorithm.HS256.getJcaName());
}
}
}
return KEY;
}
private String getUUIDFromJWT(String jwtToken) {
if ("null".equals(jwtToken) || StringUtils.isBlank(jwtToken)) {
return null;
}
try {
Map<String, Object> jwtClaims = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwtToken).getBody();
return MapUtils.getString(jwtClaims, LOGIN_USER_KEY);
} catch (ExpiredJwtException e) {
log.error("{}已过期", jwtToken);
} catch (Exception e) {
log.error("{}", e);
}
return null;
}
}
5. token 过滤器 —— 校验 token
@Component
public class TokenFilter extends OncePerRequestFilter {
public static final String TOKEN_KEY = "token";
@Autowired
private TokenService tokenService;
@Autowired
private UserDetailsService userDetailsService;
private static final Long MINUTES_10 = 10 * 60 * 1000L;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//获取请求中的token
String token = getToken(request);
if (StringUtils.isNotBlank(token)) {
//根据JwtToken获取redis中的user信息
LoginUser loginUser = tokenService.getLoginUser(token);
if (loginUser != null) {
loginUser = checkLoginTime(loginUser);
//根据用户拥有的权限列表生成授权token
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser,
null, loginUser.getAuthorities());
//将user信息存储至上下文中,可随意获取
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
/**
* 校验时间<br>
* 过期时间与当前时间对比,临近过期10分钟内的话,自动刷新缓存
*
* @param loginUser
* @return
*/
private LoginUser checkLoginTime(LoginUser loginUser) {
long expireTime = loginUser.getExpireTime();
long currentTime = System.currentTimeMillis();
if (expireTime - currentTime <= MINUTES_10) {
String token = loginUser.getToken();
loginUser = (LoginUser) userDetailsService.loadUserByUsername(loginUser.getUsername());
loginUser.setToken(token);
tokenService.refresh(loginUser);
}
return loginUser;
}
/**
* 根据参数或者header获取token
*
* @param request
* @return
*/
public static String getToken(HttpServletRequest request) {
String token = request.getParameter(TOKEN_KEY);
if (StringUtils.isBlank(token)) {
token = request.getHeader(TOKEN_KEY);
}
return token;
}
}
6. 重写 loadUserByUsername 方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Autowired
private PermissionDao permissionDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = userService.getUser(username);
if (sysUser == null) {
throw new AuthenticationCredentialsNotFoundException("用户名不存在");
} else if (sysUser.getStatus() == Status.LOCKED) {
throw new LockedException("用户被锁定,请联系管理员");
} else if (sysUser.getStatus() == Status.DISABLED) {
throw new DisabledException("用户已作废");
}
LoginUser loginUser = new LoginUser();
BeanUtils.copyProperties(sysUser, loginUser);
List<Permission> permissions = permissionDao.listByUserId(sysUser.getId());
loginUser.setPermissions(permissions);
return loginUser;
}
}
7. 配置详解
- UsernamePasswordAuthenticationFilter 类
- 该类的 attemptAuthentication 方法默认拦截 post 请求的 login 方法,从 request 中获取 username 和 password 字段进行用户名和密码校验;
- 校验完成后通过 loadUserByUsername 方法获取用户信息并存储至 authentication 中,loadUserByUsername 方法需要我们重写;
- 校验成功通过 eventPublisher.publishAuthenticationSuccess 发布事件,触发 AuthenticationSuccessHandler 的 onAuthenticationSuccess 方法。自定义登录成功的事件处理方式,如生成 token。
- token 生成
- 创建一个随机字符串 uuid 作为key,值为 user 信息,将 userDetail 存入 redis;
- 创建 JwtToken ,并将 uuid 存入JwtToken 的 claim 部分,用于后续从redis 获取 user;
- 将 JwtToken 返回前端,之后每次请求都要在请求头携带 JwtToken 。
- token 过滤器
- 拦截除设置外的其他请求,获取请求头中携带的 JwtToken,根据 JwtToken 获取 redis 中存储的 user 信息,并刷新 JwtToken 过期时长;
- 根据用户信息及其拥有的权限列表生成授权凭证UsernamePasswordAuthenticationToken,并将凭证信息存储至上下文中,方便之后从上下文中获取用户信息;
- 授权凭证存储至上下文后,我们就可以通过 @PreAuthorize(“hasAuthority(‘sys:user:query’)”) 注解进行权限校验。而页面级的权限管理需要配合前端一起实现,即后端返回用户有权限的页面名称和url等,前端渲染在左侧菜单列。
二. 全局 Ajax 设置
请求需要在 header 中携带 token,可以通过全局统一配置来实现,避免每个请求都需要写一遍,后续再引入配置文件即可。下面的配置中还对错误码进行了统一封装。
$.ajaxSetup({
cache : false,
headers : {
"token" : localStorage.getItem("token")
},
error : function(xhr, textStatus, errorThrown) {
var msg = xhr.responseText;
var response = JSON.parse(msg);
var code = response.code;
var message = response.message;
if (code == 400) {
layer.msg(message);
} else if (code == 401) {
localStorage.removeItem("token");
location.href = '/login.html';
} else if (code == 403) {
console.log("未授权:" + message);
layer.msg('未授权');
} else if (code == 500) {
layer.msg('系统错误:' + message);
}
}
});
三. 跨域配置
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 跨域支持
*
* @return
*/
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedMethods("*");
}
};
}
}
四. 定时任务管理
实现的样式,前端新增定时任务,填写cron表达式,指定定时任务需要执行的方法,后台插入一条定时任务数据并执行。

quartz.properties 配置:
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 15000
JobConfig.java:
@Configuration
public class JobConfig {
public static final String KEY = "applicationContextSchedulerContextKey";
@Bean("adminQuartzScheduler")
public SchedulerFactoryBean quartzScheduler(DataSource dataSource) {
SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean();
try {
quartzScheduler.setQuartzProperties(
PropertiesLoaderUtils.loadProperties(new ClassPathResource("quartz.properties")));
} catch (IOException e) {
e.printStackTrace();
}
quartzScheduler.setDataSource(dataSource);
quartzScheduler.setOverwriteExistingJobs(true);
//使得可以通过key获取上下文
quartzScheduler.setApplicationContextSchedulerContextKey(KEY);
quartzScheduler.setStartupDelay(10);
return quartzScheduler;
}
}
任务触发时默认执行 executeInternal 方法:
public class SpringBeanJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
try {
ApplicationContext applicationContext = (ApplicationContext) context.getScheduler().getContext()
.get(JobConfig.KEY);
JobService jobService = applicationContext.getBean(JobService.class);
//实际执行的方法
jobService.doJob(context.getJobDetail().getJobDataMap());
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
保存、删除、执行任务:
@Service
public class JobServiceImpl implements JobService {
private static final Logger log = LoggerFactory.getLogger("adminLogger");
@Autowired
private Scheduler scheduler;
@Autowired
private ApplicationContext applicationContext;
private static final String JOB_DATA_KEY = "JOB_DATA_KEY";
@Autowired
private JobDao jobDao;
@Override
public void saveJob(JobModel jobModel) {
//检查传进入的bean和method是否存在
checkJobModel(jobModel);
String name = jobModel.getJobName();
JobKey jobKey = JobKey.jobKey(name);
JobDetail jobDetail = JobBuilder.newJob(SpringBeanJob.class).storeDurably()
.withDescription(jobModel.getDescription()).withIdentity(jobKey).build();
jobDetail.getJobDataMap().put(JOB_DATA_KEY, jobModel);
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(jobModel.getCron());
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity(name).withSchedule(cronScheduleBuilder)
.forJob(jobKey).build();
try {
boolean exists = scheduler.checkExists(jobKey);
if (exists) {
scheduler.rescheduleJob(new TriggerKey(name), cronTrigger);
scheduler.addJob(jobDetail, true);
} else {
scheduler.scheduleJob(jobDetail, cronTrigger);
}
JobModel model = jobDao.getByName(name);
if (model == null) {
jobDao.save(jobModel);
} else {
jobDao.update(jobModel);
}
} catch (SchedulerException e) {
log.error("新增或修改job异常", e);
}
}
@Override
public void doJob(JobDataMap jobDataMap) {
JobModel jobModel = (JobModel) jobDataMap.get(JOB_DATA_KEY);
String beanName = jobModel.getSpringBeanName();
String methodName = jobModel.getMethodName();
Object object = applicationContext.getBean(beanName);
try {
log.info("job:bean:{},方法名:{}", beanName, methodName);
Method method = object.getClass().getDeclaredMethod(methodName);
method.invoke(object);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 删除job
*
* @throws SchedulerException
*/
@Override
public void deleteJob(Long id) throws SchedulerException {
JobModel jobModel = jobDao.getById(id);
if (jobModel.getIsSysJob() != null && jobModel.getIsSysJob()) {
throw new IllegalArgumentException("该job是系统任务,不能删除,因为此job是在代码里初始化的,删除该类job请先确保相关代码已经去除");
}
String jobName = jobModel.getJobName();
JobKey jobKey = JobKey.jobKey(jobName);
scheduler.pauseJob(jobKey);
scheduler.unscheduleJob(new TriggerKey(jobName));
scheduler.deleteJob(jobKey);
jobModel.setStatus(0);
jobDao.update(jobModel);
}
}
6236

被折叠的 条评论
为什么被折叠?



