目前流行的前后端分离让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有效性 引用的*一个答案里的处理方式
* @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
Spring Boot整合实战Spring Security JWT权限鉴权系统的更多相关文章
-
Spring Boot(十四):spring boot整合shiro-登录认证和权限管理
Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...
-
(转)Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理
http://www.ityouknow.com/springboot/2017/06/26/spring-boot-shiro.html 这篇文章我们来学习如何使用 Spring Boot 集成 A ...
-
Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理
这篇文章我们来学习如何使用 Spring Boot 集成 Apache Shiro .安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求.在 Java 领域一般有 Spring S ...
-
Spring Boot整合shiro-登录认证和权限管理
原文地址:http://www.ityouknow.com/springboot/2017/06/26/springboot-shiro.html 这篇文章我们来学习如何使用Spring Boot集成 ...
-
Spring Boot 整合 Shiro-登录认证和权限管理
这篇文章我们来学习如何使用 Spring Boot 集成 Apache Shiro .安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求.在 Java 领域一般有 Spring S ...
-
Spring Boot (十三): Spring Boot 整合 RabbitMQ
1. 前言 RabbitMQ 是一个消息队列,说到消息队列,大家可能多多少少有听过,它主要的功能是用来实现应用服务的异步与解耦,同时也能起到削峰填谷.消息分发的作用. 消息队列在比较主要的一个作用是用 ...
-
Spring Boot整合Thymeleaf视图层
目录 Spring Boot整合Thymeleaf Spring Boot整合Thymeleaf 的项目步骤 Thymeleaf 语法详解 Spring Boot整合Thymeleaf Spring ...
-
Spring Boot 整合视图层技术,application全局配置文件
目录 Spring Boot 整合视图层技术 Spring Boot 整合jsp Spring Boot 整合freemarker Spring Boot 整合视图层技术 Spring Boot 整合 ...
-
Spring Boot中使用 Spring Security 构建权限系统
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置的Bean,为应用系统提供声明式的安全 ...
随机推荐
-
Contest20140705 testA 二分
testA 输入文件: testA.in 输出文件testA.out 时限2000ms 问题描述: 有一个城市拥有N个节点,被M条有权无向路径连接.现在你要在一个地方(可以在路径上当然也可以在节点上) ...
-
正式学习React(五) Reactjs 的 PropTypes 使用方法
propTypes 使用來規範元件Props的型別與必需狀態 var Test = React.createClass({ propTypes: { // required requiredFunc: ...
-
容易上手-类似ERP系统 简单特效
今天大概简单写一个效果, 这个效果 很容易 上手的: html: <style type="text/css">.menu_list ul{display:none;} ...
-
BZOJ 2100: [Usaco2010 Dec]Apple Delivery( 最短路 )
跑两遍最短路就好了.. 话说这翻译2333 ---------------------------------------------------------------------- #includ ...
-
团队作业4——第一次项目冲刺(Alpha版本)4.25
团队作业4--第一次项目冲刺(Alpha版本) Day four: 会议照片 每日站立会议: 项目进展 今天是项目的Alpha敏捷冲刺的第四天,先大概整理下昨天已完成的任务以及今天计划完成的任务.今天 ...
-
利用ajax获取网页表单数据,并存储到数据库之一(使用JDBC)
所谓JDBC就是利用java与数据库相连接的技术,从数据库获取既有的信息或者把网页上的信息存储到数据库. 这里简单的介绍公司的一个小项目中的一部分,由于代码较多,所以用图片形式进行展示.源码请查看源码 ...
-
SQLServer之创建Transact-SQL DDL触发器
DDL触发器原理 DDL 触发器用于响应各种数据定义语言 (DDL) 事件. 这些事件主要与以关键字 CREATE.ALTER.DROP.GRANT.DENY.REVOKE 或 UPDATE STAT ...
-
MySQL关于日志配置安全整改及处理方法
[环境介绍] 系统环境:Linux + mysql 5.7.18 + 主从复制架构 [背景描述] 需求:MySQL数据库都有每年的集团安全整改,常常要求弱口令扫描,基线扫描,漏洞扫描等等.对于MySQ ...
-
在Linux下使用gcc编译mesa文件报undefined reference to symbol &#39;sin@@GLIBC_2.2.5和DSO missing from command line两个错误的解决方案
一.概述 在Linux系统下使用gcc编译用C语言写的mesa的示例程序. 环境:Ubuntu Server 18.04.1 二.问题的出现 在Ubuntu下安装好mesa所需的库文件,将目标文件从g ...
-
csdn 不登录浏览全文 chrome 浏览器
1将此文章存到书签栏. 2 右键点击保存到书签栏的这个书签,然后点击修改. 3 名称改为:CSDN查看全文,网址改为: javascript:$("#article_content" ...