此项目为前后端分离项目mybatisplus+SpringBoot项目整合SpringSecurity及JWT实现登录认证
在pom.xml中添加项目依赖
<!--SpringSecurity依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--Hutool Java工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.5.7</version>
</dependency>
添加JWT token的工具类
package com.ff.jwt;
import com.ff.result.ResultCode;
import com.ff.result.ResultObj;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JwtToken生成的工具类
*
*/
public class JWTUtil {
public static String createToken(UserDetails userDetails){
// token由三部分组成:头部、有效负载,签名
// 1.头部信息
Map<String,Object> headerMap = new HashMap<>();
headerMap.put("type","JWT");
headerMap.put("alg","HS256");
// 2.有效负载
Map<String,Object> payload = new HashMap<>();
payload.put("username",userDetails.getUsername());
payload.put("date",new Date());
// 有效时间
long timeMillis = System.currentTimeMillis();
long endTime = timeMillis+6000000;
// 3.签名
String token = Jwts.builder().setHeader(headerMap)
.setClaims(payload)
.setExpiration(new Date(endTime))
.signWith(SignatureAlgorithm.HS256, "jq1223")
.compact();
return token;
}
/**
* 从token中获取JWT中的负载
*/
public static ResultObj verifyToken(String token) {
try {
Claims claims = Jwts.parser().setSigningKey("jq1223")
.parseClaimsJws(token)
.getBody();
return ResultObj.success(claims);
} catch (Exception e) {
return ResultObj.error(ResultCode.TOKEN_ERROR);
}
}
/**
* 从token中获取登录用户名
*/
public static String getUsername(String token){
try {
Claims claims = Jwts.parser().setSigningKey("jq1223")
.parseClaimsJws(token)
.getBody();
return (String) claims.get("username");
} catch (Exception e) {
return null;
}
}
}
添加SpringSecurity的配置类
package com.fh.config.security;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.fh.config.security.bo.AdminUserDetails;
import com.fh.config.security.filter.JwtAuthenticationTokenFilter;
import com.fh.config.security.filter.RestAuthenticationEntryPoint;
import com.fh.config.security.filter.RestfulAccessDeniedHandler;
import com.fh.resource.model.Resource;
import com.fh.role.model.Role;
import com.fh.user.model.Admin;
import com.fh.user.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.Filter;
import java.util.List;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AdminService adminService;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
/**
* 类似于拦截器,配置哪些方法需要拦截,哪些方法不需要拦截
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()// 由于使用的是JWT,我们这里不需要csrf
.disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 基于token,所以不需要session
.and()
.authorizeRequests() // 哪些请求需要放开
.antMatchers("/login/**","/upload/**")// 对登录注册要允许匿名访问
.permitAll()
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
.anyRequest() // 其他请求都需要拦截
.authenticated();
// 禁用缓存
http.headers().cacheControl();
//添加 JWT Filter
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未知授权和登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
@Bean
protected JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() {
return new JwtAuthenticationTokenFilter();
}
/**
* 登录和认证
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
@Bean
protected PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 当前用户所拥有的角色和权限信息
* @return
*/
@Bean
@Override
protected UserDetailsService userDetailsService() {
return username ->{
QueryWrapper<Admin> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username",username);
Admin admin = adminService.getOne(queryWrapper);
if (admin != null){
List<Resource> resourceList = adminService.queryResourceList(admin.getId());
List<Role> roleList = adminService.queryRoleList(admin.getId());
return new AdminUserDetails(admin,resourceList,roleList);
}
throw new UsernameNotFoundException("用户不存在");
};
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
相关依赖及方法说明
configure(HttpSecurity httpSecurity):用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
configure(AuthenticationManagerBuilder auth):用于配置UserDetailsService及PasswordEncoder;
RestfulAccessDeniedHandler:当用户没有访问权限时的处理器,用于返回JSON格式的处理结果;
RestAuthenticationEntryPoint:当未登录或token失效时,返回JSON格式的结果;
UserDetailsService:SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现;
PasswordEncoder:SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder;
JwtAuthenticationTokenFilter:在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录。
添加RestfulAccessDeniedHandler
package com.ff.config.security.filter;
import cn.hutool.json.JSONUtil;
import com.ff.result.ResultCode;
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;
/**
* 当访问接口没有权限时,自定义的返回结果
*
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(ResultCode.NO_PERMISSION));
response.getWriter().flush();
}
}
添加RestAuthenticationEntryPoint
package com.ff.config.security.filter;
import cn.hutool.json.JSONUtil;
import com.ff.result.ResultCode;
import com.ff.result.ResultObj;
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;
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("utf-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(ResultCode.TOKEN_ERROR));
response.getWriter().flush();
}
}
添加AdminUserDetails
package com.ff.config.security.bo;
import com.ff.resource.model.Resource;
import com.ff.role.model.Role;
import com.ff.user.model.Admin;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* SpringSecurity需要的用户详情
*/
public class AdminUserDetails implements UserDetails {
private Admin admin;
private List<Resource> resourceList;
private List<Role> roleList;
public AdminUserDetails(Admin admin, List<Resource> resourceList, List<Role> roleList) {
this.admin = admin;
this.resourceList = resourceList;
this.roleList = roleList;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> list = new ArrayList<>();
resourceList.forEach(resource -> {
list.add(new SimpleGrantedAuthority(resource.getKeyword()));
});
roleList.forEach(role -> {
list.add(new SimpleGrantedAuthority(role.getKeyword()));
});
return list;
}
@Override
public String getPassword() {
return admin.getPassword();
}
@Override
public String getUsername() {
return admin.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return admin.getStatus().equals(1);
}
}
添加JwtAuthenticationTokenFilter
在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。
package com.ff.config.security.filter;
import com.ff.jwt.JWTUtil;
import com.ff.result.ResultObj;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
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;
import java.util.concurrent.TimeUnit;
/**
* JWT登录授权过滤器
*
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private RedisTemplate redisTemplate;
//token key
private static final String pre_token="ACCESS_TOKEN:";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader("Authorization-token");
if (StringUtils.isNotBlank(token)){
ResultObj resultObj = JWTUtil.verifyToken(token);
if (resultObj.getCode() == 200){
String username=JWTUtil.getUsername(token);
UserDetails userDetails=userDetailsService.loadUserByUsername(username);
String accessKey=pre_token+userDetails.getUsername()+":"+token;
if(redisTemplate.hasKey(accessKey)){
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
Long currentTime=System.currentTimeMillis();
//续签:
redisTemplate.opsForValue().set(accessKey,currentTime);
redisTemplate.expire(accessKey,2, TimeUnit.HOURS);
}
}
}
filterChain.doFilter(request,response);
}
}
添加LoginController 类
摘自项目源码地址
添加LoginController 类
package com.ff.controller;
import com.ff.result.ResultCode;
import com.ff.result.ResultObj;
import com.ff.user.model.Admin;
import com.ff.user.service.AdminService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("login")
@CrossOrigin
public class LoginController {
@Autowired
private AdminService adminService;
@PostMapping("logins")
public ResultObj login(String username,String password){
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)){
return ResultObj.error(ResultCode.USER_PWD_ISNULL);
}
//判断用户名是否存在
Admin admin = adminService.queryUserByUsername(username);
if (admin == null) {
return ResultObj.error(ResultCode.USER_NOEXIST);
}
String token = adminService.login(username,password);
if (token == null){
ResultObj.error(ResultCode.TOKEN_ERROR);
}
return ResultObj.success(token);
}
}
添加AdminServiceImpl 类
package com.ff.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ff.jwt.JWTUtil;
import com.ff.resource.model.Resource;
import com.ff.result.ResultCode;
import com.ff.result.ResultObj;
import com.ff.role.model.Role;
import com.ff.user.mapper.AdminMapper;
import com.ff.user.model.Admin;
import com.ff.user.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* <p>
* 后台用户表 服务实现类
* </p>
*
* @author @jq
* @since 2021-03-09
*/
@Service
public class AdminServiceImpl extends ServiceImpl<AdminMapper, Admin> implements AdminService {
@javax.annotation.Resource
private AdminMapper adminMapper;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
private static final String pre_token = "ACCESS_TOKEN:";
@Autowired
private RedisTemplate redisTemplate;
@Override
public List<Resource> queryResourceList(Long id) {
return adminMapper.queryResourceList(id);
}
@Override
public List<Role> queryRoleList(Long id) {
return adminMapper.queryRoleList(id);
}
@Override
public String login(String username, String password) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (!passwordEncoder.matches(password,userDetails.getPassword())){
throw new BadCredentialsException("密码错误");
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
String token = JWTUtil.createToken(userDetails);
long timeMillis = System.currentTimeMillis();
String accessKey = pre_token+userDetails.getUsername()+":"+token;
redisTemplate.opsForValue().set(accessKey,timeMillis);
redisTemplate.expire(accessKey,2,TimeUnit.MINUTES);
return token;
}
判断用户名是否存在
@Override
public Admin queryUserByUsername(String username) {
return adminMapper.selectOne(new QueryWrapper<Admin>().eq("username",username));
}
}
添加AdminMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ff.user.mapper.AdminMapper">
<select id="queryResourceList" parameterType="java.lang.Long" resultType="com.ff.resource.model.Resource">
SELECT DISTINCT
r.id,
r.keyword
FROM
ums_admin_role_relation ar
LEFT JOIN ums_role_resource_relation rr ON ar.role_id = rr.role_id
LEFT JOIN ums_resource r ON rr.resource_id = r.id
WHERE
ar.admin_id = #{
id}
AND r.keyword IS NOT NULL
</select>
<select id="queryRoleList" parameterType="java.lang.Long" resultType="com.fh.role.model.Role">
SELECT
r.id,
r.keyword
FROM
ums_admin_role_relation ar
LEFT JOIN ums_role r ON ar.role_id = r.id
WHERE
ar.admin_id = #{
id}
</select>
</mapper>
可能会报错的地方:
解决方案:查看需要查询的接口中有没有需要判断不能为空的条件
前端request.js
import axios from 'axios';
const service = axios.create({
timeout: 500000000000
});
service.interceptors.request.use(config=>{
// alert(localStorage.getItem("token"));
config.headers.common["Authorization-token"]=localStorage.getItem("token");
return config;
})
service.interceptors.response.use(response=>{
var code=response.data.code;
if( code != 200){
router.push("/login");
return Promise.reject('error');
}
return response;
})
export default service;