SpringBoot2.X Security JWT 登录授权认证
目前在java后端的开发项目中,授权认证这个功能基本是必备的,这里只讲常用的登录授权认证,Spring Security + JWT(java web token)。
Spring Security 官网介绍
https://spring.io/projects/spring-security
image.png
官网文档说明:
https://docs.spring.io/spring-security/site/docs/5.4.1/reference/html5/
image.png
SpringSecurity是一个强大的可高度定制的认证和授权框架,
对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,
像所有的Spring项目一样,它对自定义需求具有强大的扩展性。
JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,
由于使用了数字签名,所以是可信任和安全的。
JWT的组成
JWT token的格式:header.payload.signature
1.header中用于存放签名的生成算法 {"alg": "HS512"}
2.payload中用于存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981393,"exp":1489684781}
3.signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
直接上案例:
gihub 代码地址:https://github.com/LaiHouWen/SpringSecurityJwt
项目借鉴 开源项目 mall商城,http://www.macrozheng.com/#/architect/mall_arch_04,剥离出来的。
先准备一张用户表admin,角色表role,用户权限表permission,用户和角色关系表,用户与角色是多对多关系admin_role_relation,用户角色和权限关系表,角色与权限是多对多关系role_permission_relation,用户和权限关系表admin_permission_relation。
admin表sql
DROP TABLE IF EXISTS `admin`;
CREATE TABLE `admin` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`email` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`nick_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`login_time` datetime(0) NULL DEFAULT NULL COMMENT '最后登录时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
role表SQL
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
`description` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`admin_count` int(11) NULL DEFAULT NULL COMMENT '后台用户数量',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间'
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色表' ROW_FORMAT = Dynamic;
permission表sql
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`pid` bigint(20) NULL DEFAULT NULL COMMENT '父级权限id',
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
`value` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限值',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间'
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户权限表' ROW_FORMAT = Dynamic;
admin_permission_relation表sql
DROP TABLE IF EXISTS `admin_permission_relation`;
CREATE TABLE `admin_permission_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`admin_id` bigint(20) NULL DEFAULT NULL,
`permission_id` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户和权限关系表' ROW_FORMAT = Dynamic;
admin_role_relation表sql
DROP TABLE IF EXISTS `admin_role_relation`;
CREATE TABLE `admin_role_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`admin_id` bigint(20) NULL DEFAULT NULL,
`role_id` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户和角色关系表' ROW_FORMAT = Dynamic;
role_permission_relation表sql
DROP TABLE IF EXISTS `role_permission_relation`;
CREATE TABLE `role_permission_relation` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`role_id` bigint(20) NULL DEFAULT NULL,
`permission_id` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色和权限关系表' ROW_FORMAT = Dynamic;
1 . pom导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--SpringSecurity依赖配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis 生成器 -->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.7</version>
</dependency>
<!--MyBatis分页插件-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.10</version>
</dependency>
<!--集成druid连接池-->
<!--web监控 http://127.0.0.1:8080/druid/index.html -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--Swagger-UI API文档生产工具-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--Hutool Java工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.4.7</version>
</dependency>
<!--JWT(Json Web Token)登录支持-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--参数校验 javax.validation.constraints-->
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.1.5.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
</dependencies>
2. application配置
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/testmall?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
redis:
# redis服务器地址
host: localhost
# redis数据库索引(默认为0)
database: 0
# redis服务器连接端口
port: 6379
# redis 服务器连接密码 默认为空
password:
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
# 连接超时时间(毫秒)
timeout: 3000ms
# 自定义 redis key
redis:
# 自定义 redis key
key:
prefix:
authCode: "portal:authCode:"
expire:
#验证码超期时间
authCode: 120
#自定义字段
jwt:
secret: mySecret
# jwt超期限时间 60*60*24
expiration: 604800
# JWT存储的请求头
tokenHeader: Authorization
# JWT负载中拿到开头
tokenHead: Bearer
#mybatis
mybatis:
#mapper 映射文件地址
mapper-locations:
- classpath:mapper/*.xml
- classpath:com/**/mapper/*.xml
- security 配置
/**
* SpringSecurity的配置
*
* UserDetails:SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),
* 需要自行实现;
*/
@Configuration
//开启Security
@EnableWebSecurity
//保证post之前的注解可以使用
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UmsAdminService adminService;
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler;
@Autowired
private RestAuthenticationEntryPoint restAuthenticationEntryPoint;
/**
* 用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器;
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf()
//由于使用的是JWT,我们这里不需要csrf
.disable()//
//基于token,所以不需要session
.sessionManagement()
//STATELESS不创建httpsession并不使用
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)//
.and()
.authorizeRequests()
//允许对于网站静态资源的无授权访问
.antMatchers(HttpMethod.GET,
"/",
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/swagger-resources/**",
"/v2/api-docs")
.permitAll()
//对登录注册要允许匿名访问
.antMatchers("/admin/login",
"/admin/register")
.permitAll()
//跨域请求会先进行一次options请求
.antMatchers(HttpMethod.OPTIONS)
.permitAll()
//测试时全部运行访问
// .antMatchers("/**")
// .permitAll()
//除上面外的所有请求全部需要鉴权认证
.anyRequest()
.authenticated();
//禁用缓存
http.headers().cacheControl();
//添加JWT filter 前置拦截
http.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
//添加自定义未授权和未登录结果返回
http.exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthenticationEntryPoint);
}
/**
* 用于配置 UserDetailsService 及PasswordEncoder;
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
/**
* SpringSecurity定义的用于对密码进行编码及比对的接口,
* 目前使用的是BCryptPasswordEncoder;
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现;
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
//获取登录用户信息,查询用户权限
// return username -> {
// UmsAdmin admin = adminService.getAdminByUsername(username);
// if (admin !=null ){
// List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId());
// return new AdminUserDetails(admin,permissionList);
// }
// throw new UsernameNotFoundException("用户名或密码错误");
// };
return new UserDetailsServiceImpl();
}
/**
* 在用户名和密码校验前添加的过滤器,
* 如果有jwt的token,会自行根据token信息进行登录。
* @return
*/
@Bean
public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){
return new JwtAuthenticationTokenFilter();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
拦截器
JwtAuthenticationTokenFilter
/**
* JWT登录授权过滤器
* 在用户名和密码校验前添加的过滤器,如果请求中有jwt的token且有效,
* 会取出token中的用户名,然后调用SpringSecurity的API进行登录操作。
* 请求 前置 过滤 token
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private static final Logger LOGGER =LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
@Autowired
private UmsAdminService umsAdminService;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.tokenHeader}")
private String tokenHeader;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(this.tokenHeader);
LOGGER.info("checking authHeader:{}",authHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)){
//The part after "Bearer "
String authToken = authHeader.substring(this.tokenHead.length());
LOGGER.info("checking authToken:{}",authToken);
String username = jwtTokenUtil.getUserNameFromToken(authToken);
LOGGER.info("checking username:{}",username);
//SecurityContextHolder.getContext().getAuthentication() == null
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
//
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
LOGGER.info("authenticated user:{}", username);
//打印用户的权限
LOGGER.info("authenticated Authorities:{}", userDetails.getAuthorities().size());
//这个需要设置
//如果没有这个会报 :Full authentication is required to access this resource
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request,response);
}
}
RestAuthenticationEntryPoint
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*/
@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger LOGGER = LoggerFactory.getLogger(RestAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
LOGGER.info("RestAuthenticationEntryPoint commence 未登录或者token失效访问接口");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(e.getMessage())));
response.getWriter().flush();
}
}
RestfulAccessDeniedHandler
/**
* 当访问接口没有权限时,自定义的返回结果
*
*/
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RestfulAccessDeniedHandler.class);
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException e) throws IOException, ServletException {
LOGGER.info("RestfulAccessDeniedHandler handle 当访问接口没有权限时,自定义的返回结果");
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
//没有访问权限
response.getWriter().println(JSONUtil.parse(CommonResult.failed(e.getMessage())));
response.getWriter().flush();
}
}
service
UmsAdminService
/**
*
*/
public interface UmsAdminService {
//根据用户名获取后台管理员
UmsAdmin getAdminByUsername(String username);
//注册功能
UmsAdmin register(UmsAdmin umsAdminParam);
/**
* 登录功能
* @param username
* @param password
* @return 生成的JWT的token
*/
String login(String username,String password);
//获取用户所有权限(包括角色权限和+-权限)
List<UmsPermission> getPermissionList(Long adminId);
}
UmsAdminServiceImpl
/**
*UmsAdminService实现类
*/
@Service
public class UmsAdminServiceImpl implements UmsAdminService {
private static final Logger LOGGER = LoggerFactory.getLogger(UmsAdminServiceImpl.class);
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private UmsAdminMapper adminMapper;
@Autowired
private UmsAdminRoleRelationDao adminRoleRelationDao;
//通过用户名获取管理员信息
@Override
public UmsAdmin getAdminByUsername(String username) {
UmsAdminExample example = new UmsAdminExample();
example.createCriteria().andUsernameEqualTo(username);
List<UmsAdmin> adminList = adminMapper.selectByExample(example);
if (adminList != null && adminList.size()>0){
return adminList.get(0);
}
return null;
}
@Override
public UmsAdmin register(UmsAdmin umsAdminParam) {
UmsAdmin umsAdmin = new UmsAdmin();
BeanUtils.copyProperties(umsAdminParam,umsAdmin);
umsAdmin.setCreateTime(new Date());
umsAdmin.setStatus(1);
//查询是否有相同用户名的用户
UmsAdminExample example = new UmsAdminExample();
example.createCriteria().andUsernameEqualTo(umsAdmin.getUsername());
List<UmsAdmin> umsAdminList = adminMapper.selectByExample(example);
if (umsAdminList.size()>0){
return null;
}
//将密码进行加密操作
String encodePassword = passwordEncoder.encode(umsAdmin.getPassword());
umsAdmin.setPassword(encodePassword);
adminMapper.insert(umsAdmin);
return umsAdmin;
}
@Override
public String login(String username, String password) {
String token = null;
try {
AdminUserDetails userDetails = null;
List<UmsPermission> permissionList=null;
//获取用户的信息
UmsAdmin admin = getAdminByUsername(username);
if (admin==null){
throw new BadCredentialsException("用户不存在");
}
permissionList = getPermissionList(admin.getId());
userDetails = new AdminUserDetails(admin,permissionList);
if (!passwordEncoder.matches(password,userDetails.getPassword())){
throw new BadCredentialsException("密码不正确");
}
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
// //避免跨多个线程的竞争条件
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
token = jwtTokenUtil.generateToken(admin);
// UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// if (!passwordEncoder.matches(password, userDetails.getPassword())) {
// throw new BadCredentialsException("密码不正确");
// }
// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// SecurityContextHolder.getContext().setAuthentication(authentication);
// token = jwtTokenUtil.generateToken(userDetails);
}catch (AuthenticationException e){
LOGGER.warn("登录异常:{}",e.getMessage());
}
return token;
}
@Override
public List<UmsPermission> getPermissionList(Long id) {
return adminRoleRelationDao.getPermissionList(id);
}
}
UserDetailsServiceImpl
/**
* UserDetailsService 实现 返回权限查询
*/
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger logger = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private UmsAdminService umsAdminService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UmsAdmin admin = umsAdminService.getAdminByUsername(username);
if (admin !=null ){
List<UmsPermission> permissionList = umsAdminService.getPermissionList(admin.getId());
return new AdminUserDetails(admin,permissionList);
}
throw new UsernameNotFoundException("用户名或密码错误");
}
}
/**
* 用户与角色管理自定义Dao
* mapper映射
*/
public interface UmsAdminRoleRelationDao {
//获取用户所有权限(包括+-权限)
public List<UmsPermission> getPermissionList(@Param("adminId") Long adminId);
}
UmsAdminRoleRelationDao.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.mall.tiny04.dao.UmsAdminRoleRelationDao">
<select id="getPermissionList" resultMap="com.mall.tiny04.mbg.mapper.UmsPermissionMapper.BaseResultMap">
SELECT
p.*
FROM
admin_role_relation ar
LEFT JOIN role r ON ar.role_id = r.id
LEFT JOIN role_permission_relation rp ON r.id = rp.role_id
LEFT JOIN permission p ON rp.permission_id = p.id
WHERE
ar.admin_id = #{adminId}
AND p.id IS NOT NULL
AND p.id NOT IN (
SELECT
p.id
FROM
admin_permission_relation pr
LEFT JOIN permission p ON pr.permission_id = p.id
WHERE
pr.type = - 1
AND pr.admin_id = #{adminId}
)
UNION
SELECT
p.*
FROM
admin_permission_relation pr
LEFT JOIN permission p ON pr.permission_id = p.id
WHERE
pr.type = 1
AND pr.admin_id = #{adminId}
</select>
</mapper>
controller方法上添加权限
image.png
@PostMapping("/update/{id}")
// id 是否有 update权限
@PreAuthorize("hasPermission(#id,'update')")
//是否有创建权限
//@PreAuthorize("hasAuthority('create')")
// 是否有admin权限
// @PreAuthorize("hasRole('admin')")
@ResponseBody
public CommonResult updateBrand(@PathVariable("id") Long id,
@RequestBody PmsBrand pmsBrand,
BindingResult result){
}









网友评论