Spring Boot整合实战Spring Security JWT权限鉴权系统
目前流行的前后端分离让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.Driverspring.datasource.url=jdbc:mysql://localhost:3306/spring_security?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.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#连接池最大连接数,默认是10spring.datasource.hikari.maximum-pool-size=10#此属性控制从池返回的连接的默认自动提交行为,默认值:truespring.datasource.hikari.auto-commit=true#连接池名字spring.datasource.hikari.pool-name=HwHikariCP#此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟spring.datasource.hikari.max-lifetime=1800000#数据库连接超时时间,默认30秒,即30000spring.datasource.hikari.connection-timeout=30000spring.datasource.hikari.connection-test-query=SELECT 1# JWT配置# 自定义 服务端根据secret生成tokenjwt.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);//生成基本的resultMapgc.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@Getterpublic 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;}//返回分配给用户的角色列表@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> authorities = new ArrayList<>();authorities.add(new SimpleGrantedAuthority(role.getName()));return authorities;}//账户是否未过期,过期无法验证@Overridepublic boolean isAccountNonExpired() {return true;}//指定用户是否解锁,锁定的用户无法进行身份验证@Overridepublic boolean isAccountNonLocked() {return true;}//指示是否已过期的用户的凭据(密码),过期的凭据防止认证@Overridepublic boolean isCredentialsNonExpired() {return true;}//是否可用 ,禁用的用户不能身份验证@Overridepublic 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*/@Componentpublic 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@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter {@Value("${jwt.header}")private String tokenHeader;@Value("${jwt.tokenHead}")private String authTokenStart;@Resourceprivate JwtTokenUtil jwtTokenUtil;@Overrideprotected 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 {@Overridepublic 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@Componentpublic class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {private static final long serialVersionUID = -8970718410437077606L;@Overridepublic 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 {@Autowiredprivate JwtAuthenticationEntryPoint unauthorizedHandler;@Autowiredprivate AccessDeniedHandler accessDeniedHandler;@Autowiredprivate JwtAuthenticationTokenFilter authenticationTokenFilter;/*** 解决 无法直接注入 AuthenticationManager* @return* @throws Exception*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Autowiredpublic WebSecurityConfig(JwtAuthenticationEntryPoint unauthorizedHandler,@Qualifier("RestAuthenticationAccessDeniedHandler") AccessDeniedHandler accessDeniedHandler,JwtAuthenticationTokenFilter authenticationTokenFilter) {this.unauthorizedHandler = unauthorizedHandler;this.accessDeniedHandler = accessDeniedHandler;this.authenticationTokenFilter = authenticationTokenFilter;}/*** 配置策略** @param httpSecurity* @throws Exception*/@Overrideprotected 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 filterhttpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);}@Autowiredpublic void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {auth// 设置UserDetailsService.userDetailsService(userDetailsService())// 使用BCrypt进行密码的hash.passwordEncoder(passwordEncoder());auth.eraseCredentials(false);}/*** 装载BCrypt密码编码器 密码加密** @return*/@Beanpublic BCryptPasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 登陆身份认证** @return*/@Override@Beanpublic UserDetailsService userDetailsService() {return new UserDetailsService() {@Autowiredprivate IUserService userService;@Autowiredprivate IRoleService roleService;@Overridepublic 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*/@Controllerpublic class LoginController {@Autowiredprivate 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")@ResponseBodypublic 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")@ResponseBodypublic 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,为应用系统提供声明式的安全 ...
随机推荐
- Prototype Chain
参考资料: <javascript权威指南 第六版> <javascript高级程序设计 第二版> 写在前面的话 所谓的原型链就是一个一个的对象通过其__proto__属性连接 ...
- Opencv - Android 配置安装
1.道具们: windows 7 64位 OpenCV-2.4.6-android-sdk-r2 ( http://sourceforge.net/projects/opencvlibrary/fil ...
- leetcode 226. Invert Binary Tree(递归)
Invert a binary tree. 4 / \ 2 7 / \ / \ 1 3 6 9 to 4 / \ 7 2 / \ / \ 9 6 3 1 Trivia:This problem was ...
- ACM学习历程—POJ 3764 The xor-longest Path(xor && 字典树 && 贪心)
题目链接:http://poj.org/problem?id=3764 题目大意是在树上求一条路径,使得xor和最大. 由于是在树上,所以两个结点之间应有唯一路径. 而xor(u, v) = xor( ...
- Tensorflow Summary用法
本文转载自:https://www.cnblogs.com/lyc-seu/p/8647792.html Tensorflow Summary用法 tensorboard 作为一款可视化神器,是学习t ...
- Linux 下网卡参数配置
目录 Linux 下网卡参数配置 第一种:修改 interfaces 文件 网卡配置实例 回环参数配置 DHCP方式配置 静态 IP 地址分配 无线网卡配置 March 17, 2015 7:48 P ...
- python文件操作 seek(),tell()
seek():移动文件读取指针到指定位置 tell():返回文件读取指针的位置 seek()的三种模式: (1)f.seek(p,0) 移动当文件第p个字节处,绝对位置 (2)f.seek(p,1) ...
- POJ3468(树状数组区间维护)
A Simple Problem with Integers Time Limit: 5000MS Memory Limit: 131072K Total Submissions: 89818 ...
- 【转】 Pro Android学习笔记(五八):Preferences(2):CheckBoxPreference
目录(?)[-] CheckBox Preference xml文件 设备的存贮文件 复合preference 在ListPreference的例子中显示的是单选,如果是多选,可采用CheckBoxP ...
- struts 文件上传示例
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io ...