目前流行的前后端分离让Java程序员可以更加专注的做好后台业务逻辑的功能实现,提供如返回Json格式的数据接口就可以。像以前做项目的安全认证基于 session 的登录拦截,属于后端全栈式的开发的模式, 前后端分离鲜明的,前端不要接触过多的业务逻辑,都由后端解决, 服务端通过 JSON字符串,告诉前端用户有没有登录、认证,前端根据这些提示跳转对应的登录页、认证页等, 今天就Spring Boot整合Spring Security JWT实现登录认证以及权限认证,本文简单介绍用户和用户角色的权限问题
一. Spring Security简介
1.简介
一个能够为基于Spring的企业应用系统提供声明式的安全訪问控制解决方式的安全框架(简单说是对访问权限进行控制嘛),应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。 spring security的主要核心功能为 认证和授权,所有的架构也是基于这两个核心功能去实现的。
2.认证过程
用户使用用户名和密码进行登录。 Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken。 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证。 AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象。 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext。 上述介绍的就是 Spring Security 的认证过程。在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定。
二. JWT
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。具体的还是自行百度吧
三. 搭建系统
本系统使用技术栈
数据库: MySql
连接池: Hikari
持久层框架: MyBatis-plus
安全框架: Spring Security
安全传输工具: JWT
Json解析: fastjson
1.建数据库
设计用户和角色 设计一个最简角色表 role,包括 角色ID和 角色名称 role
-
Create Table: CREATE TABLE `role` ( -
`id` int(11) DEFAULT NULL, -
`name` char(10) DEFAULT NULL -
) ENGINE=InnoDB DEFAULT CHARSET=utf8
设计一个最简用户表 user,包括 用户ID, 用户名, 密码 user
-
Create Table: CREATE TABLE `user` ( -
`id` int(11) DEFAULT NULL, -
`username` char(10) DEFAULT NULL, -
`password` char(100) DEFAULT NULL -
) ENGINE=InnoDB DEFAULT CHARSET=utf8
关联表 user_role
-
Create Table: CREATE TABLE `user_role` ( -
`user_id` int(11) DEFAULT NULL, -
`role_id` int(11) DEFAULT NULL -
) ENGINE=InnoDB DEFAULT CHARSET=utf8
2.新建Spring Boot工程
引入相关依赖
-
<dependency> -
<groupId>org.springframework.boot</groupId> -
<artifactId>spring-boot-starter-security</artifactId> -
</dependency> -
<dependency> -
<groupId>org.springframework.security</groupId> -
<artifactId>spring-security-test</artifactId> -
<scope>test</scope> -
</dependency> -
-
<!--MySQL驱动--> -
<dependency> -
<groupId>mysql</groupId> -
<artifactId>mysql-connector-java</artifactId> -
<scope>runtime</scope> -
</dependency> -
<!--Mybatis-Plus--> -
<dependency> -
<groupId>com.baomidou</groupId> -
<artifactId>mybatis-plus</artifactId> -
<version>3.0.6</version> -
</dependency> -
<dependency> -
<groupId>com.baomidou</groupId> -
<artifactId>mybatis-plus-boot-starter</artifactId> -
<version>3.0.6</version> -
</dependency> -
<!-- 模板引擎 --> -
<dependency> -
<groupId>org.apache.velocity</groupId> -
<artifactId>velocity-engine-core</artifactId> -
<version>2.0</version> -
</dependency> -
<!--JWT--> -
<dependency> -
<groupId>io.jsonwebtoken</groupId> -
<artifactId>jjwt</artifactId> -
<version>0.9.0</version> -
</dependency> -
<!--lombok--> -
<dependency> -
<groupId>org.projectlombok</groupId> -
<artifactId>lombok</artifactId> -
<optional>true</optional> -
</dependency> -
<!--阿里fastjson--> -
<dependency> -
<groupId>com.alibaba</groupId> -
<artifactId>fastjson</artifactId> -
<version>1.2.4</version> -
</dependency>
配置文件
-
# 数据源 -
spring.datasource.driver-class-name=com.mysql.jdbc.Driver -
spring.datasource.url=jdbc:mysql://localhost:3306/spring_security?useUnicode=true&characterEncoding=utf-8 -
spring.datasource.username=root -
spring.datasource.password=root -
-
#mybatis-plus配置 -
#mapper对应文件 -
mybatis-plus.mapper-locations=classpath:mapper/*.xml -
#实体扫描,多个package用逗号或者分号分隔 -
mybatis-plus.typeAliasesPackage=com.li.springbootsecurity.model -
#执行的sql打印出来 开发/测试 -
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl -
-
#Hikari 连接池配置 -
#最小空闲连接数量 -
spring.datasource.hikari.minimum-idle=5 -
#空闲连接存活最大时间,默认600000(10分钟) -
spring.datasource.hikari.idle-timeout=180000 -
#连接池最大连接数,默认是10 -
spring.datasource.hikari.maximum-pool-size=10 -
#此属性控制从池返回的连接的默认自动提交行为,默认值:true -
spring.datasource.hikari.auto-commit=true -
#连接池名字 -
spring.datasource.hikari.pool-name=HwHikariCP -
#此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟 -
spring.datasource.hikari.max-lifetime=1800000 -
#数据库连接超时时间,默认30秒,即30000 -
spring.datasource.hikari.connection-timeout=30000 -
spring.datasource.hikari.connection-test-query=SELECT 1 -
-
# JWT配置 -
# 自定义 服务端根据secret生成token -
jwt.secret=mySecret -
# 头部 -
jwt.header=Authorization -
# token有效时间 -
jwt.expiration=604800 -
# token头部 -
jwt.tokenHead=Bearer
2.代码生成
这里简单说明下: 建表完成后 使用mybatis-plus代码生成(不了解的自行了解 后面会出教程 本文不做过多介绍)
生成代码
-
package com.li.springbootsecurity.code; -
-
-
import com.baomidou.mybatisplus.annotation.DbType; -
import com.baomidou.mybatisplus.generator.AutoGenerator; -
import com.baomidou.mybatisplus.generator.config.DataSourceConfig; -
import com.baomidou.mybatisplus.generator.config.GlobalConfig; -
import com.baomidou.mybatisplus.generator.config.PackageConfig; -
import com.baomidou.mybatisplus.generator.config.StrategyConfig; -
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; -
-
-
/** -
* @Author 李号东 -
* @Description mybatis-plus自动生成 -
* @Date 08:07 2019-03-17 -
* @Param -
* @return -
**/ -
public class MyBatisPlusGenerator { -
-
public static void main(String[] args) { -
// 代码生成器 -
AutoGenerator mpg = new AutoGenerator(); -
-
//1. 全局配置 -
GlobalConfig gc = new GlobalConfig(); -
gc.setOutputDir("/Volumes/李浩东的移动硬盘/LiHaodong/springboot-security/src/main/java"); -
gc.setOpen(false); -
gc.setFileOverride(true); -
gc.setBaseResultMap(true);//生成基本的resultMap -
gc.setBaseColumnList(false);//生成基本的SQL片段 -
gc.setAuthor("lihaodong");// 作者 -
mpg.setGlobalConfig(gc); -
-
//2. 数据源配置 -
DataSourceConfig dsc = new DataSourceConfig(); -
dsc.setDbType(DbType.MYSQL); -
dsc.setDriverName("com.mysql.jdbc.Driver"); -
dsc.setUsername("root"); -
dsc.setPassword("root"); -
dsc.setUrl("jdbc:mysql://127.0.0.1:3306/test"); -
mpg.setDataSource(dsc); -
-
//3. 策略配置globalConfiguration中 -
StrategyConfig strategy = new StrategyConfig(); -
strategy.setTablePrefix("");// 此处可以修改为您的表前缀 -
strategy.setNaming(NamingStrategy.underline_to_camel);// 表名生成策略 -
strategy.setSuperEntityClass("com.li.springbootsecurity.model"); -
strategy.setInclude("role"); // 需要生成的表 -
strategy.setEntityLombokModel(true); -
strategy.setRestControllerStyle(true); -
strategy.setControllerMappingHyphenStyle(true); -
-
mpg.setStrategy(strategy); -
-
//4. 包名策略配置 -
PackageConfig pc = new PackageConfig(); -
pc.setParent("com.li.springbootsecurity"); -
pc.setEntity("model"); -
mpg.setPackageInfo(pc); -
-
// 执行生成 -
mpg.execute(); -
-
} -
}
3.User类
简单的用户模型
-
package com.li.springbootsecurity.model; -
-
import com.baomidou.mybatisplus.annotation.IdType; -
import com.baomidou.mybatisplus.annotation.TableId; -
import com.baomidou.mybatisplus.annotation.TableName; -
import com.baomidou.mybatisplus.extension.activerecord.Model; -
import lombok.*; -
import lombok.experimental.Accessors; -
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; -
-
/** -
* 用户类 -
* @author lihaodong -
* @since 2019-03-14 -
*/ -
@Setter -
@Getter -
@ToString -
@TableName("user") -
public class User extends Model<User>{ -
-
private static final long serialVersionUID = 1L; -
-
private Integer id; -
-
private String username; -
-
private String password; -
-
}
4.Role类
-
package com.li.springbootsecurity.model; -
-
import com.baomidou.mybatisplus.annotation.TableName; -
import com.baomidou.mybatisplus.extension.activerecord.Model; -
import lombok.*; -
import lombok.experimental.Accessors; -
-
/** -
* 角色类 -
* @author lihaodong -
* @since 2019-03-14 -
*/ -
@Setter -
@Getter -
@Builder -
@TableName("role") -
public class Role extends Model<User> { -
-
private static final long serialVersionUID = 1L; -
-
private Integer id; -
-
private String name; -
-
-
}
4.用户服务类
-
package com.li.springbootsecurity.service; -
-
import com.li.springbootsecurity.bo.ResponseUserToken; -
import com.li.springbootsecurity.model.User; -
import com.baomidou.mybatisplus.extension.service.IService; -
import com.li.springbootsecurity.security.SecurityUser; -
-
/** -
* <p> -
* 用户服务类 -
* </p> -
* -
* @author lihaodong -
* @since 2019-03-14 -
*/ -
public interface IUserService extends IService<User> { -
-
-
/** -
* 通过用户名查找用户 -
* -
* @param username 用户名 -
* @return 用户信息 -
*/ -
User findByUserName(String username); -
-
/** -
* 登陆 -
* @param username -
* @param password -
* @return -
*/ -
ResponseUserToken login(String username, String password); -
-
-
/** -
* 根据Token获取用户信息 -
* @param token -
* @return -
*/ -
SecurityUser getUserByToken(String token); -
}
5.安全用户模型 主要用来用户身份权限认证类 登陆身份认证
-
package com.li.springbootsecurity.security; -
-
import com.li.springbootsecurity.model.Role; -
import com.li.springbootsecurity.model.User; -
import lombok.Getter; -
import lombok.Setter; -
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.Date; -
import java.util.List; -
-
/** -
* @Author 李号东 -
* @Description 用户身份权限认证类 登陆身份认证 -
* @Date 13:29 2019-03-16 -
* @Param -
* @return -
**/ -
@Setter -
@Getter -
public class SecurityUser extends User implements UserDetails { -
private static final long serialVersionUID = 1L; -
-
private Integer id; -
private String username; -
private String password; -
private Role role; -
private Date lastPasswordResetDate; -
-
public SecurityUser(Integer id, String username, Role role, String password) { -
this.id = id; -
this.username = username; -
this.password = password; -
this.role = role; -
} -
-
public SecurityUser(String username, String password, Role role) { -
this.username = username; -
this.password = password; -
this.role = role; -
} -
-
public SecurityUser(Integer id, String username, String password) { -
this.id = id; -
this.username = username; -
this.password = password; -
} -
-
-
//返回分配给用户的角色列表 -
@Override -
public Collection<? extends GrantedAuthority> getAuthorities() { -
List<GrantedAuthority> authorities = new ArrayList<>(); -
authorities.add(new SimpleGrantedAuthority(role.getName())); -
return authorities; -
} -
-
//账户是否未过期,过期无法验证 -
@Override -
public boolean isAccountNonExpired() { -
return true; -
} -
-
//指定用户是否解锁,锁定的用户无法进行身份验证 -
@Override -
public boolean isAccountNonLocked() { -
return true; -
} -
-
//指示是否已过期的用户的凭据(密码),过期的凭据防止认证 -
@Override -
public boolean isCredentialsNonExpired() { -
return true; -
} -
-
//是否可用 ,禁用的用户不能身份验证 -
@Override -
public boolean isEnabled() { -
return true; -
} -
}
此处所创建的 SecurityUser类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即通过继承 UserDetails,即可实现 Security中相关的安全功能。
6.创建JWT工具类
主要用于对 JWT Token进行各项操作,比如生成Token、验证Token、刷新Token等
-
package com.li.springbootsecurity.utils; -
-
import com.alibaba.fastjson.JSON; -
import com.li.springbootsecurity.model.Role; -
import com.li.springbootsecurity.security.SecurityUser; -
import io.jsonwebtoken.CompressionCodecs; -
import org.springframework.beans.factory.annotation.Value; -
import org.springframework.security.core.GrantedAuthority; -
import org.springframework.security.core.userdetails.UserDetails; -
import org.springframework.stereotype.Component; -
-
import java.util.*; -
import java.util.concurrent.ConcurrentHashMap; -
-
import io.jsonwebtoken.Claims; -
import io.jsonwebtoken.Jwts; -
import io.jsonwebtoken.SignatureAlgorithm; -
-
/** -
* @Classname JwtTokenUtil -
* @Description JWT工具类 -
* @Author 李号东 lihaodongmail@163.com -
* @Date 2019-03-14 14:54 -
* @Version 1.0 -
*/ -
@Component -
public class JwtTokenUtil { -
-
private static final String ROLE_REFRESH_TOKEN = "ROLE_REFRESH_TOKEN"; -
private static final String CLAIM_KEY_USER_ID = "user_id"; -
private static final String CLAIM_KEY_AUTHORITIES = "scope"; -
-
private Map<String, String> tokenMap = new ConcurrentHashMap<>(32); -
-
/** -
* 密钥 -
*/ -
@Value("${jwt.secret}") -
private String secret; -
-
/** -
* 有效期 -
*/ -
@Value("${jwt.expiration}") -
private Long accessTokenExpiration; -
-
/** -
* 刷新有效期 -
*/ -
@Value("${jwt.expiration}") -
private Long refreshTokenExpiration; -
-
private final SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS256; -
-
-
/** -
* 根据token 获取用户信息 -
* @param token -
* @return -
*/ -
public SecurityUser getUserFromToken(String token) { -
SecurityUser userDetail; -
try { -
final Claims claims = getClaimsFromToken(token); -
int userId = getUserIdFromToken(token); -
String username = claims.getSubject(); -
String roleName = claims.get(CLAIM_KEY_AUTHORITIES).toString(); -
Role role = Role.builder().name(roleName).build(); -
userDetail = new SecurityUser(userId, username, role, ""); -
} catch (Exception e) { -
userDetail = null; -
} -
return userDetail; -
} -
-
/** -
* 根据token 获取用户ID -
* @param token -
* @return -
*/ -
private int getUserIdFromToken(String token) { -
int userId; -
try { -
final Claims claims = getClaimsFromToken(token); -
userId = Integer.parseInt(String.valueOf(claims.get(CLAIM_KEY_USER_ID))); -
} catch (Exception e) { -
userId = 0; -
} -
return userId; -
} -
-
-
/** -
* 根据token 获取用户名 -
* @param token -
* @return -
*/ -
public String getUsernameFromToken(String token) { -
String username; -
try { -
final Claims claims = getClaimsFromToken(token); -
username = claims.getSubject(); -
} catch (Exception e) { -
username = null; -
} -
return username; -
} -
-
/** -
* 根据token 获取生成时间 -
* @param token -
* @return -
*/ -
public Date getCreatedDateFromToken(String token) { -
Date created; -
try { -
final Claims claims = getClaimsFromToken(token); -
created = claims.getIssuedAt(); -
} catch (Exception e) { -
created = null; -
} -
return created; -
} -
-
-
/** -
* 生成令牌 -
* -
* @param userDetail 用户 -
* @return 令牌 -
*/ -
public String generateAccessToken(SecurityUser userDetail) { -
Map<String, Object> claims = generateClaims(userDetail); -
claims.put(CLAIM_KEY_AUTHORITIES, authoritiesToArray(userDetail.getAuthorities()).get(0)); -
return generateAccessToken(userDetail.getUsername(), claims); -
} -
-
/** -
* 根据token 获取过期时间 -
* @param token -
* @return -
*/ -
private Date getExpirationDateFromToken(String token) { -
Date expiration; -
try { -
final Claims claims = getClaimsFromToken(token); -
expiration = claims.getExpiration(); -
} catch (Exception e) { -
expiration = null; -
} -
return expiration; -
} -
-
public Boolean canTokenBeRefreshed(String token, Date lastPasswordReset) { -
final Date created = getCreatedDateFromToken(token); -
return !isCreatedBeforeLastPasswordReset(created, lastPasswordReset) -
&& (!isTokenExpired(token)); -
} -
-
/** -
* 刷新令牌 -
* -
* @param token 原令牌 -
* @return 新令牌 -
*/ -
public String refreshToken(String token) { -
String refreshedToken; -
try { -
final Claims claims = getClaimsFromToken(token); -
refreshedToken = generateAccessToken(claims.getSubject(), claims); -
} catch (Exception e) { -
refreshedToken = null; -
} -
return refreshedToken; -
} -
-
-
/** -
* 验证token 是否合法 -
* @param token token -
* @param userDetails 用户信息 -
* @return -
*/ -
public boolean validateToken(String token, UserDetails userDetails) { -
SecurityUser userDetail = (SecurityUser) userDetails; -
final long userId = getUserIdFromToken(token); -
final String username = getUsernameFromToken(token); -
return (userId == userDetail.getId() -
&& username.equals(userDetail.getUsername()) -
&& !isTokenExpired(token) -
); -
} -
-
/** -
* 根据用户信息 重新获取token -
* @param userDetail -
* @return -
*/ -
public String generateRefreshToken(SecurityUser userDetail) { -
Map<String, Object> claims = generateClaims(userDetail); -
// 只授于更新 token 的权限 -
String[] roles = new String[]{JwtTokenUtil.ROLE_REFRESH_TOKEN}; -
claims.put(CLAIM_KEY_AUTHORITIES, JSON.toJSON(roles)); -
return generateRefreshToken(userDetail.getUsername(), claims); -
} -
-
public void putToken(String userName, String token) { -
tokenMap.put(userName, token); -
} -
-
public void deleteToken(String userName) { -
tokenMap.remove(userName); -
} -
-
public boolean containToken(String userName, String token) { -
return userName != null && tokenMap.containsKey(userName) && tokenMap.get(userName).equals(token); -
} -
-
/*** -
* 解析token 信息 -
* @param token -
* @return -
*/ -
private Claims getClaimsFromToken(String token) { -
Claims claims; -
try { -
claims = Jwts.parser() -
.setSigningKey(secret) -
.parseClaimsJws(token) -
.getBody(); -
} catch (Exception e) { -
claims = null; -
} -
return claims; -
} -
-
/** -
* 生成失效时间 -
* @param expiration -
* @return -
*/ -
private Date generateExpirationDate(long expiration) { -
return new Date(System.currentTimeMillis() + expiration * 1000); -
} -
-
/** -
* 判断令牌是否过期 -
* -
* @param token 令牌 -
* @return 是否过期 -
*/ -
private Boolean isTokenExpired(String token) { -
final Date expiration = getExpirationDateFromToken(token); -
return expiration.before(new Date()); -
} -
-
/** -
* 生成时间是否在最后修改时间之前 -
* @param created 生成时间 -
* @param lastPasswordReset 最后修改密码时间 -
* @return -
*/ -
private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) { -
return (lastPasswordReset != null && created.before(lastPasswordReset)); -
} -
-
-
private Map<String, Object> generateClaims(SecurityUser userDetail) { -
Map<String, Object> claims = new HashMap<>(16); -
claims.put(CLAIM_KEY_USER_ID, userDetail.getId()); -
return claims; -
} -
-
/** -
* 生成token -
* @param subject 用户名 -
* @param claims -
* @return -
*/ -
private String generateAccessToken(String subject, Map<String, Object> claims) { -
return generateToken(subject, claims, accessTokenExpiration); -
} -
-
private List authoritiesToArray(Collection<? extends GrantedAuthority> authorities) { -
List<String> list = new ArrayList<>(); -
for (GrantedAuthority ga : authorities) { -
list.add(ga.getAuthority()); -
} -
return list; -
} -
-
-
private String generateRefreshToken(String subject, Map<String, Object> claims) { -
return generateToken(subject, claims, refreshTokenExpiration); -
} -
-
-
/** -
* 生成token -
* @param subject 用户名 -
* @param claims -
* @param expiration 过期时间 -
* @return -
*/ -
private String generateToken(String subject, Map<String, Object> claims, long expiration) { -
return Jwts.builder() -
.setClaims(claims) -
.setSubject(subject) -
.setId(UUID.randomUUID().toString()) -
.setIssuedAt(new Date()) -
.setExpiration(generateExpirationDate(expiration)) -
.compressWith(CompressionCodecs.DEFLATE) -
.signWith(SIGNATURE_ALGORITHM, secret) -
.compact(); -
} -
-
}
7.创建Token过滤器,用于每次外部对接口请求时的Token处理
-
package com.li.springbootsecurity.config; -
-
import com.li.springbootsecurity.security.SecurityUser; -
import com.li.springbootsecurity.utils.JwtTokenUtil; -
import lombok.extern.slf4j.Slf4j; -
import org.apache.commons.lang3.StringUtils; -
import org.springframework.beans.factory.annotation.Value; -
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -
import org.springframework.security.core.context.SecurityContextHolder; -
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; -
import org.springframework.stereotype.Component; -
import org.springframework.web.filter.OncePerRequestFilter; -
-
import javax.annotation.Resource; -
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.Date; -
-
/** -
* @Author 李号东 -
* @Description token过滤器来验证token有效性 引用的stackoverflow一个答案里的处理方式 -
* @Date 00:32 2019-03-17 -
* @Param -
* @return -
**/ -
@Slf4j -
@Component -
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { -
-
@Value("${jwt.header}") -
private String tokenHeader; -
-
@Value("${jwt.tokenHead}") -
private String authTokenStart; -
-
@Resource -
private JwtTokenUtil jwtTokenUtil; -
-
-
@Override -
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { -
-
String authToken = request.getHeader(this.tokenHeader); -
System.out.println(authToken); -
if (StringUtils.isNotEmpty(authToken) && authToken.startsWith(authTokenStart)) { -
authToken = authToken.substring(authTokenStart.length()); -
log.info("请求" + request.getRequestURI() + "携带的token值:" + authToken); -
//如果在token过期之前触发接口,我们更新token过期时间,token值不变只更新过期时间 -
//获取token生成时间 -
Date createTokenDate = jwtTokenUtil.getCreatedDateFromToken(authToken); -
log.info("createTokenDate: " + createTokenDate); -
-
} else { -
// 不按规范,不允许通过验证 -
authToken = null; -
} -
String username = jwtTokenUtil.getUsernameFromToken(authToken); -
log.info("JwtAuthenticationTokenFilter[doFilterInternal] checking authentication " + username); -
-
if (jwtTokenUtil.containToken(username, authToken) && username != null && SecurityContextHolder.getContext().getAuthentication() == null) { -
SecurityUser userDetail = jwtTokenUtil.getUserFromToken(authToken); -
if (jwtTokenUtil.validateToken(authToken, userDetail)) { -
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetail, null, userDetail.getAuthorities()); -
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); -
log.info(String.format("Authenticated userDetail %s, setting security context", username)); -
SecurityContextHolder.getContext().setAuthentication(authentication); -
} -
} -
chain.doFilter(request, response); -
} -
}
8.创建RestAuthenticationAccessDeniedHandler 自定义权限不足处理类
-
package com.li.springbootsecurity.config; -
-
import com.li.springbootsecurity.bo.ResultCode; -
import com.li.springbootsecurity.bo.ResultJson; -
import com.li.springbootsecurity.bo.ResultUtil; -
import lombok.extern.slf4j.Slf4j; -
import org.springframework.security.access.AccessDeniedException; -
import org.springframework.security.web.access.AccessDeniedHandler; -
import org.springframework.stereotype.Component; -
-
import javax.servlet.http.HttpServletRequest; -
import javax.servlet.http.HttpServletResponse; -
import java.io.IOException; -
import java.io.PrintWriter; -
-
/** -
* @Author 李号东 -
* @Description 权限不足处理类 返回403 -
* @Date 00:31 2019-03-17 -
* @Param -
* @return -
**/ -
@Slf4j -
@Component("RestAuthenticationAccessDeniedHandler") -
public class RestAuthenticationAccessDeniedHandler implements AccessDeniedHandler { -
-
@Override -
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException { -
StringBuilder msg = new StringBuilder("请求: "); -
msg.append(httpServletRequest.getRequestURI()).append(" 权限不足,无法访问系统资源."); -
log.info(msg.toString()); -
ResultUtil.writeJavaScript(httpServletResponse, ResultCode.FORBIDDEN, msg.toString()); -
} -
}
9.创建JwtAuthenticationEntryPoint 认证失败处理类
-
package com.li.springbootsecurity.config; -
-
import com.li.springbootsecurity.bo.ResultCode; -
import com.li.springbootsecurity.bo.ResultUtil; -
import lombok.extern.slf4j.Slf4j; -
import org.springframework.security.authentication.BadCredentialsException; -
import org.springframework.security.authentication.InsufficientAuthenticationException; -
import org.springframework.security.core.AuthenticationException; -
import org.springframework.security.web.AuthenticationEntryPoint; -
import org.springframework.stereotype.Component; -
-
import javax.servlet.http.HttpServletRequest; -
import javax.servlet.http.HttpServletResponse; -
import java.io.IOException; -
import java.io.Serializable; -
-
/** -
* @Author 李号东 -
* @Description 认证失败处理类 返回401 -
* @Date 00:32 2019-03-17 -
* @Param -
* @return -
**/ -
@Slf4j -
@Component -
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { -
-
private static final long serialVersionUID = -8970718410437077606L; -
-
@Override -
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException { -
StringBuilder msg = new StringBuilder("请求访问: "); -
msg.append(httpServletRequest.getRequestURI()).append(" 接口, 经jwt 认证失败,无法访问系统资源."); -
log.info(msg.toString()); -
log.info(e.toString()); -
// 用户登录时身份认证未通过 -
if (e instanceof BadCredentialsException) { -
log.info("用户登录时身份认证失败."); -
ResultUtil.writeJavaScript(httpServletResponse, ResultCode.UNAUTHORIZED, msg.toString()); -
} else if (e instanceof InsufficientAuthenticationException) { -
log.info("缺少请求头参数,Authorization传递是token值所以参数是必须的."); -
ResultUtil.writeJavaScript(httpServletResponse, ResultCode.NO_TOKEN, msg.toString()); -
} else { -
log.info("用户token无效."); -
ResultUtil.writeJavaScript(httpServletResponse, ResultCode.TOKEN_INVALID, msg.toString()); -
} -
-
} -
}
10.Spring Security web安全配置类编写 可以说是重中之重
这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分
-
package com.li.springbootsecurity.config; -
-
import com.li.springbootsecurity.model.Role; -
import com.li.springbootsecurity.model.User; -
import com.li.springbootsecurity.security.SecurityUser; -
import com.li.springbootsecurity.service.IRoleService; -
import com.li.springbootsecurity.service.IUserService; -
import lombok.extern.slf4j.Slf4j; -
import org.springframework.beans.factory.annotation.Autowired; -
import org.springframework.beans.factory.annotation.Qualifier; -
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.configuration.EnableWebSecurity; -
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -
import org.springframework.security.config.http.SessionCreationPolicy; -
import org.springframework.security.core.userdetails.UserDetails; -
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.web.access.AccessDeniedHandler; -
import org.springframework.security.web.authentication.AuthenticationFailureHandler; -
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; -
import org.springframework.security.web.util.matcher.RequestMatcher; -
-
-
/** -
* @Author 李号东 -
* @Description Security配置类 -
* @Date 00:36 2019-03-17 -
* @Param -
* @return -
**/ -
@Slf4j -
@Configuration -
@EnableWebSecurity //启动web安全性 -
//@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法级的权限注解 性设置后控制器层的方法前的@PreAuthorize("hasRole('admin')") 注解才能起效 -
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { -
-
@Autowired -
private JwtAuthenticationEntryPoint unauthorizedHandler; -
-
@Autowired -
private AccessDeniedHandler accessDeniedHandler; -
-
@Autowired -
private JwtAuthenticationTokenFilter authenticationTokenFilter; -
-
-
/** -
* 解决 无法直接注入 AuthenticationManager -
* @return -
* @throws Exception -
*/ -
@Bean -
@Override -
public AuthenticationManager authenticationManagerBean() throws Exception { -
return super.authenticationManagerBean(); -
} -
-
@Autowired -
public WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler, -
@Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler, -
JwtAuthenticationTokenFilter authenticationTokenFilter) { -
this.unauthorizedHandler = unauthorizedHandler; -
this.accessDeniedHandler = accessDeniedHandler; -
this.authenticationTokenFilter = authenticationTokenFilter; -
} -
-
-
/** -
* 配置策略 -
* -
* @param httpSecurity -
* @throws Exception -
*/ -
@Override -
protected void configure(HttpSecurity httpSecurity) throws Exception { -
httpSecurity -
// 由于使用的是JWT,我们这里不需要csrf -
.csrf().disable() -
// 权限不足处理类 -
.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and() -
// 认证失败处理类 -
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() -
// 基于token,所以不需要session -
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() -
.authorizeRequests() -
// 对于登录login要允许匿名访问 -
.antMatchers("/login","/favicon.ico").permitAll() -
// 需要拥有admin权限 -
.antMatchers("/user").hasAuthority("admin") -
// 除上面外的所有请求全部需要鉴权认证 -
.anyRequest().authenticated(); -
-
// 禁用缓存 -
httpSecurity.headers().cacheControl(); -
// 添加JWT filter -
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); -
} -
-
@Autowired -
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { -
auth -
// 设置UserDetailsService -
.userDetailsService(userDetailsService()) -
// 使用BCrypt进行密码的hash -
.passwordEncoder(passwordEncoder()); -
auth.eraseCredentials(false); -
} -
-
/** -
* 装载BCrypt密码编码器 密码加密 -
* -
* @return -
*/ -
@Bean -
public BCryptPasswordEncoder passwordEncoder() { -
return new BCryptPasswordEncoder(); -
} -
-
/** -
* 登陆身份认证 -
* -
* @return -
*/ -
@Override -
@Bean -
public UserDetailsService userDetailsService() { -
return new UserDetailsService() { -
@Autowired -
private IUserService userService; -
@Autowired -
private IRoleService roleService; -
-
@Override -
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { -
log.info("登录用户:" + username); -
User user = userService.findByUserName(username); -
if (user == null) { -
log.info("登录用户:" + username + " 不存在."); -
throw new UsernameNotFoundException("登录用户:" + username + " 不存在"); -
} -
//获取用户拥有的角色 -
Role role = roleService.findRoleByUserId(user.getId()); -
return new SecurityUser(username, user.getPassword(), role); -
} -
}; -
} -
-
-
}
11.创建测试的 LoginController:
-
package com.li.springbootsecurity.controller; -
-
import com.li.springbootsecurity.bo.ResponseUseroken; -
import com.li.springbootsecurity.bo.ResultCode; -
import com.li.springbootsecurity.bo.ResultJson; -
import com.li.springbootsecurity.model.User; -
import com.li.springbootsecurity.security.SecurityUser; -
import com.li.springbootsecurity.service.IUserService; -
import org.springframework.beans.factory.annotation.Autowired; -
import org.springframework.beans.factory.annotation.Value; -
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -
import org.springframework.stereotype.Controller; -
import org.springframework.web.bind.annotation.*; -
-
import javax.servlet.http.HttpServletRequest; -
-
/** -
* @Classname LoginController -
* @Description 测试 -
* @Author 李号东 lihaodongmail@163.com -
* @Date 2019-03-16 10:06 -
* @Version 1.0 -
*/ -
@Controller -
public class LoginController { -
-
@Autowired -
private IUserService userService; -
-
@Value("${jwt.header}") -
private String tokenHeader; -
-
/** -
* @Author 李号东 -
* @Description 登录 -
* @Date 10:18 2019-03-17 -
* @Param [user] -
* @return com.li.springbootsecurity.bo.ResultJson<com.li.springbootsecurity.bo.ResponseUserToken> -
**/ -
@RequestMapping(value = "/login") -
@ResponseBody -
public ResultJson<ResponseUserToken> login(User user) { -
System.out.println(user); -
ResponseUserToken response = userService.login(user.getUsername(), user.getPassword()); -
return ResultJson.ok(response); -
} -
-
/** -
* @Author 李号东 -
* @Description 获取用户信息 在WebSecurityConfig配置只有admin权限才可以访问 主要用来测试权限 -
* @Date 10:17 2019-03-17 -
* @Param [request] -
* @return com.li.springbootsecurity.bo.ResultJson -
**/ -
@GetMapping(value = "/user") -
@ResponseBody -
public ResultJson getUser(HttpServletRequest request) { -
String token = request.getHeader(tokenHeader); -
if (token == null) { -
return ResultJson.failure(ResultCode.UNAUTHORIZED); -
} -
SecurityUser securityUser = userService.getUserByToken(token); -
return ResultJson.ok(securityUser); -
} -
-
public static void main(String[] args) { -
String password = "admin"; -
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4); -
String enPassword = encoder.encode(password); -
System.out.println(enPassword); -
} -
}
接下来启动工程,实验测试看看效果
权限测试 访问/use 接口 由于test用户角色是普通用户没有权限去访问
测试说明
1. 数据库数据
数据库已经新建两个用户 一个test 一个admin 密码都是admin
角色 一个 admin管理员 一个genreal普通用户
user_role进行关联
2. 管理员登录测试
接下来进行用户登录,并获得后台向用户颁发的JWT Token
权限测试
(1) 不带token访问接口
(2) 带token访问
3. 普通用户登录
权限测试 访问/use 接口 由于test用户角色是普通用户没有权限去访问
经过一系列的测试过程, 最后还是很满意的 前后端分离的权限系统设计就这样做好了
不管是什么架构 涉及到安全问题总会比其他框架更难一点
后面会进行优化 以及进行集成微服务oauth 2.0 敬请期待吧
本文涉及的东西还是很多的 有的不好理解 建议大家去GitHUb获取源码进行分析
源码下载: https://github.com/LiHaodong888/SpringBootLearn
浙公网安备 33010602011771号