Springboot-cli 开发脚手架系列
文章目录
简介
Springboo配置Shiro+jwt进行登录校验,权限认证,附demo演示。
-
什么是JWT
jwt 全称JSON Web Tokens,是目前最流行的跨域身份验证解决方案。 -
验证流程
-
这种基于token的认证方式相比传统的session认证方式更节约服务器资源,并且对移动端和分布式更加友好。其优点如下:
1.支持跨域访问
:cookie是无法跨域的,而token由于没有用到cookie(前提是将token放到请求头中),所以跨域后不会存在信息丢失问题
2.无状态
:token机制在服务端不需要存储session信息,因为token自身包含了所有登录用户的信息,所以可以减轻服务端压力
3.更适用CDN
:可以通过内容分发网络请求服务端的所有资料
4.更适用于移动端
:当客户端是非浏览器平台时,cookie是不被支持的,此时采用token认证方式会简单很多
5.无需考虑CSRF
:由于不再依赖cookie,所以采用token认证方式不会发生CSRF,所以也就无需考虑CSRF的防御
1. Springboot实战完整教程
pom.xml
依赖
<!-- Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
<!-- Shiro使用Spring框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.0</version>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- jwt java api支持 -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
application.yml
配置文件中配置jwt的秘钥及有效时间
server:
port: 9999
jwt:
# 密钥
secret: xxxxx.xxxx.xxxx
# 有效期(秒)
expire: 86400
2. 封装jwt工具
TokenProvider
用于生成及校验token
/**
* token管理
*
* @author Ding
*/
@Slf4j
@Component
public class JwtProvider {
@Value("${jwt.expire}")
private Integer expire;
@Value("${jwt.secret}")
private String secret;
/**
* 生成token
*
* @param userId 用户id
*/
public String createToken(Object userId) {
return createToken(userId, "");
}
/**
* 生成token
*
* @param userId 用户id
* @param clientId 用于区别客户端,如移动端,网页端,此处可根据自己业务自定义
*/
public String createToken(Object userId, String clientId) {
Date validity = new Date((new Date()).getTime() + expire * 1000);
return Jwts.builder()
// 代表这个JWT的主体,即它的所有人
.setSubject(String.valueOf(userId))
// 代表这个JWT的签发主体
.setIssuer("")
// 是一个时间戳,代表这个JWT的签发时间;
.setIssuedAt(new Date())
// 代表这个JWT的接收对象
.setAudience(clientId)
// 放入用户id
.claim("userId", userId)
// 自定义信息
.claim("xx", "")
.signWith(SignatureAlgorithm.HS512, this.getSecretKey())
.setExpiration(validity)
.compact();
}
/**
* 校验token
*/
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(this.getSecretKey()).parseClaimsJws(authToken);
return true;
} catch (Exception e) {
log.error("无效的token:" + authToken);
}
return false;
}
/**
* 解码token
*/
public Claims decodeToken(String token) {
if (validateToken(token)) {
Claims claims = Jwts.parser().setSigningKey(this.getSecretKey()).parseClaimsJws(token).getBody();
// 客户端id
String clientId = claims.getAudience();
// 用户id
Object userId = claims.get("userId");
log.info("token有效,userId:{}", userId);
return claims;
}
log.error("***token无效***");
return null;
}
private String getSecretKey() {
return Base64.getEncoder().encodeToString(secret.getBytes(StandardCharsets.UTF_8));
}
}
3. shiro和JWT整合
- 先介绍我们要用到的类
JwtToken
:自定义的token类,用以代替shiro原生的UsernamePasswordToken
JwtRealm
:自定义的Realm对象,处理token校验。
ShiroDefaultSubjectFactory
:自定义的subjectFactory,继承于DefaultSubjectFactory,用于生产subject对象,由于我们是无状态登录,所以重写该类弃用shiro内部的session。
ShiroFilter
:需要进行jwt认证的API接口经过的过滤器。
ShiroCofig
:Shiro的核心配置类,用于配置安全管理器(securityManager),授权过滤器(ShiroFilterFactoryBean)。
JwtToken
自定义的token类
public class JwtToken implements AuthenticationToken {
private final String token;
public JwtToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
JwtRealm
自定义的Realm对象,处理token校验。
/**
* 处理token校验
*
* @author ding
*/
@Component
@RequiredArgsConstructor
public class JwtRealm extends AuthorizingRealm {
private final JwtProvider jwtProvider;
/**
* 多重写一个support
* 标识这个Realm是专门用来验证JwtToken
* 不负责验证其他的token(UsernamePasswordToken)
*/
@Override
public boolean supports(AuthenticationToken token) {
// 这个token就是从过滤器中传入的jwtToken
return token instanceof JwtToken;
}
/**
* 自定义授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String token = (String) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// 默认给一个user角色
authorizationInfo.addRole("user");
return authorizationInfo;
}
/**
* 自定义认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String jwt = (String) authenticationToken.getPrincipal();
// 解码token
Claims claims = jwtProvider.decodeToken(jwt);
if (claims == null) {
throw new IncorrectCredentialsException("Authorization token is invalid");
}
// claims放入全局Subject中
return new SimpleAuthenticationInfo(claims, jwt, "JwtRealm");
}
}
ShiroDefaultSubjectFactory
重写该类弃用shiro内部的session。
/**
* 不创建shiro内部的session
* @author ding
*/
public class ShiroDefaultSubjectFactory extends DefaultSubjectFactory {
@Override
public Subject createSubject(SubjectContext context) {
// 不创建shiro内部的session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
ShiroFilter
API接口经过的过滤器
/**
* 需要认证的url经过该过滤器
*
* @author ding
*/
@Slf4j
public class ShiroFilter extends AccessControlFilter {
/**
* 跨域支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 对跨域OPTIONS请求放行
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpResponse.setStatus(HttpStatus.OK.value());
return true;
}
return super.preHandle(request, response);
}
/**
* 是否允许通过,因为是无状态所以默认不通过,去自动登陆,返回false,调用onAccessDenied方法
* 这里getSubject方法实际上就是获得一个subject
* 与原生shiro不同的地方在于没有对username和password进行封装
* 直接使用jwt进行认证,login方法实际上就是交给Realm进行认证
*/
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
String token = ((HttpServletRequest) servletRequest).getHeader("token");
if (token == null) {
return false;
}
try {
getSubject(servletRequest, servletResponse).login(new JwtToken(token));
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 自定义认证失败返回
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
httpResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
ResponseResult<String> resp = ResponseResult.fail(ResponseResult.RespCode.UNAUTHORIZED);
httpResponse.getWriter().write(JSON.toJSONString(resp));
return false;
}
}
ShiroCofig
核心配置类
/**
* shiro核心管理器:三大核心对象:Subject、SecurityManager、Realm
*
* @author ding
*/
@Configuration
@Slf4j
public class ShiroConfig {
/**
* 告诉shiro不创建内置的session
*/
@Bean
public SubjectFactory subjectFactory() {
return new ShiroDefaultSubjectFactory();
}
/**
* 创建安全管理器
*/
@Bean("securityManager")
public DefaultWebSecurityManager getManager(JwtRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的realm
manager.setRealm(realm);
// 关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
/**
* 授权过滤器
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilter.setSecurityManager(securityManager);
// 注册jwt过滤器,也就是将jwtFilter注册到shiro的Filter中,并在下面注册,指定除了login和logout之外的请求都先经过jwtFilter
Map<String, Filter> filterMap = new HashMap<>(3) {
{
put("anon", new AnonymousFilter());
put("jwt", new ShiroFilter());
put("logout", new LogoutFilter());
}
};
shiroFilter.setFilters(filterMap);
// 拦截器
Map<String, String> filterRuleMap = new LinkedHashMap<>(){
{
// 登录注册放行
put("/login", "anon");
put("/register", "anon");
// swagger放行
put("/swagger-ui.html", "anon");
put("/swagger-resources", "anon");
put("/v2/api-docs", "anon");
put("/webjars/springfox-swagger-ui/**", "anon");
put("/configuration/security", "anon");
put("/configuration/ui", "anon");
// 任何请求都需要经过jwt过滤器
put("/**", "jwt");
}
};
shiroFilter.setFilterChainDefinitionMap(filterRuleMap);
return shiroFilter;
}
}
- 封装响应体
ResponseResult
,用于统一json响应(可选)
/**
* 通用响应体
*
* @author qiding
*/
@Data
@Accessors(chain = true)
public class ResponseResult<T> implements Serializable {
private static final long serialVersionUID = -1L;
private Integer code;
private String message;
private T data;
public ResponseResult(Integer code, String message, T data) {
super();
this.code = code;
this.message = message;
this.data = data;
}
private static <T> ResponseResult<T> build(Integer code, String message, T data) {
return new ResponseResult<>(code, message, data);
}
public static <T> ResponseResult<T> ok() {
return new ResponseResult<>(RespCode.OK.code, RespCode.OK.message, null);
}
public static <T> ResponseResult<T> ok(T data) {
return build(RespCode.OK.code, RespCode.OK.message, data);
}
public static <T> ResponseResult<T> fail() {
return fail(RespCode.ERROR.message);
}
public static <T> ResponseResult<T> fail(String message) {
return fail(RespCode.ERROR, message);
}
public static <T> ResponseResult<T> fail(RespCode respCode) {
return fail(respCode, respCode.message);
}
public static <T> ResponseResult<T> fail(RespCode respCode, String message) {
return build(respCode.getCode(), message, null);
}
public enum RespCode {
/**
* 业务码
*/
OK(20000, "请求成功"),
MY_ERROR(20433, "自定义异常"),
UNAUTHORIZED(20401, "未授权"),
LOGIN_FAIL(20402, "账号或密码错误"),
ERROR(20400, "未知异常");
RespCode(int code, String message) {
this.code = code;
this.message = message;
}
private final int code;
private final String message;
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
}
4. 开启跨域支持
- 由于shiro是基于过滤器的,所以我们这里继承Filter ,进行跨域处理
/**
* 由于shiro是基于过滤器的,所以我们这里继承Filter ,进行跨域处理
*
* @author ding
*/
@Component
@Slf4j
public class CorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws ServletException, IOException {
HttpServletResponse response = (HttpServletResponse) res;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", request.getMethod());
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "*");
chain.doFilter(request, response);
}
}
5. 登录注册实战
- 编写
User
实体类,模拟一个用户
@Data
@Accessors(chain = true)
public class User {
/**
* id
*/
private Long userId;
/**
* 账号
*/
private String username;
/**
* 密码
*/
private String password;
}
- 封装shiro 工具类
/**
* shiro工具类
* 用于快速获取登录信息
*
* @author ding
*/
public class ShiroUtils {
/**
* md5盐
*/
private static final String SALT = "xx.com";
/**
* 获取登录信息
*/
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
/**
* 获取用户id
*
* @param <T> id类型
*/
public static <T> T getUserId(Class<T> c) {
Subject subject = getSubject();
Claims claims = (Claims) subject.getPrincipal();
return claims.get("userId", c);
}
/**
* 密码md5加密
*
* @param password 密码
*/
public static String md5(String password) {
return new Md5Hash(password, SALT, 1024).toString();
}
/**
* 密码比对
*
* @param password 未加密的密码
* @param md5password 加密过的密码
*/
public static boolean verifyPassword(String password, String md5password) {
return new Md5Hash(password, SALT, 1024).toString().equals(md5password);
}
/**
* 退出登录
*/
public static void logout() {
getSubject().logout();
}
}
- 编写测试API
/**
* @author ding
*/
@RestController
@RequiredArgsConstructor
public class LoginController {
private final JwtProvider jwtProvider;
/**
* 模拟一个数据库用户
* 账号admin
* 密码123456
*/
private final static HashMap<String, User> USER_MAP = new LinkedHashMap<>() {
{
put("admin", new User()
.setUserId(1L)
.setUsername("admin")
.setPassword(ShiroUtils.md5("123456"))
);
}
};
/**
* 登录
*/
@PostMapping(value = "/login")
public ResponseResult<String> login(@RequestParam("username") String username,
@RequestParam("password") String password) {
User user = USER_MAP.get(username);
if (Objects.isNull(user)) {
return ResponseResult.fail("用户不存在");
}
// 密码加密校验
if (ShiroUtils.verifyPassword(password, user.getPassword())) {
String token = jwtProvider.createToken(user.getUserId());
return ResponseResult.ok(token);
}
return ResponseResult.fail("账号或密码错误");
}
/**
* 注册
*/
@PostMapping(value = "/register")
public ResponseResult<String> register(@RequestParam("username") String username,
@RequestParam("password") String password) {
USER_MAP.put(username, new User()
.setUserId(USER_MAP.size() + 1L)
.setUsername(username)
// 对密码进行加密保存
.setPassword(ShiroUtils.md5(password)));
return ResponseResult.ok("注册成功");
}
/**
* 获取用户
*/
@GetMapping("/getUser")
public ResponseResult<User> getUser() {
// 获取当前登录的用户id
Long userId = ShiroUtils.getUserId(Long.class);
User user = USER_MAP.values()
.stream()
.filter(u -> u.getUserId().equals(userId))
.findFirst()
.orElseThrow();
return ResponseResult.ok(user);
}
/**
* 退出登录
*/
@GetMapping("/logout")
public ResponseResult<String> logout() {
SecurityUtils.getSubject().logout();
return ResponseResult.ok("成功退出登录");
}
}
6. 效果演示
-
注册
register
-
登录
login
-
获取用户信息
getUser
,在请求头放入登录时获取的token
7. 源码分享
本项目已收录
- Springboot-cli开发脚手架,集合springboot、springcloud各种常用框架使用案例,完善的文档,致力于让开发者快速搭建基础环境并让应用跑起来,并提供丰富的使用示例供使用者参考,帮助初学者快速上手。
- 项目源码github地址
- 项目源码国内gitee地址