项目实现了Spring Security 权限控制 + Jwt Token认证。
所有的过滤器都会实现SpringSecurityFilter安全过滤器
我理解的,就是用户凭证不再由服务端保存,而是由客户端自己保存。即客户端登陆后,将加密登陆凭证交于客户端,客户端并不明白凭证有何意义,只知道登陆需要使用。在登陆访问时我们获取到登陆凭证进行解密,获取到当前用户信息。同时用户凭证隔一段时间会失效。具体介绍可以 https://www.jianshu.com/p/12b609e40029 的介绍
文末会给出项目地址,所以基础搭建不在赘述。
1. 登陆阶段流程图。 中间省略了Spring Security 的某些调用。仅用来描绘自己代码的逻辑。 2. 登陆阶段讲解。(请自觉忽略我的背景图。。。。。)
1.我们在配置类中添加了两个自定义拦截器 JwtLoginFilter 和 JwtTokenFilter 。我们这里关注 JwtLoginFilter
2.JwtLoginFilter 继承了 UsernamePasswordAuthenticationFilter, UsernamePasswordAuthenticationFilter 是用来处理身份验证的表单提交。也就是说 我们在 JwtLoginFilter 中处理表单提交的身份信息。
我们进去 UsernamePasswordAuthenticationFilter 可以看到在其构造函数指定了拦截路径,即默认拦截 Post 请求方式的 /login 请求。我们可以在配置类中通过 formLogin().loginProcessingUrl(“XXXX”) 来指定登陆路径。 3. 在 JwtLoginFilter 中,我们获取参数username,password 来获取提交的用户名和密码,封装了凭证后进行登陆信息的校验。this.getAuthenticationManager() 来获取用户认证的管理类 。用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。我们这里的实现类即 JwtAuthenticationProvider 4. 跳转到 JwtAuthenticationProvider.authenticate 中进行逻辑处理(因为在配置类中指定了通过 authenticationProvider方法配置了校验类)。 JwtAuthenticationProvider.authenticate 的具体校验,根据注释就可以清楚了。 5. 校验成功则会调用 JwtLoginSuccessHandler.生成一个token并返回给用户
5.失败则会调用 JwtLoginFailureHandler 返回错误信息 6. 至于为什么会调用这两个类,是因为我们在配置类中进行了初始化配置: 并且在拦截链路中加入了这两个拦截器。 对于添加拦截器以下三个方法:
addFilterBefore(Filter filter, Class beforeFilter) 在 beforeFilter 之前添加 filteraddFilterAfter(Filter filter, Class afterFilter) 在 afterFilter 之后添加 filteraddFilterAt(Filter filter, Class atFilter) 在 atFilter 相同位置添加 filter, 此 filter 不覆盖 filter1. Token验证流程图 2. 代码讲解 1.由于我们在拦截链中加入了JwtLoginFilter 、JwtTokenFilter 。而 JwtLoginFilter 上面说过只拦截登陆路径。其余路径则会被 JwtTokenFilter 拦截。 2. JwtTokenFilter具体代码如下。
如果验证通过,则将token保存在Security上下文中。并进行下一步调用。 用户的请求会到达 JwtFilterInvocationSecurityMetadataSource 中。 JwtFilterInvocationSecurityMetadataSource 根据当前路径获取到有资格访问当前页面的角色列表(比如 Admin,Teacher 等)。 随后调用链路到了 JwtUrlAccessDecisionManager 中,在这里来校验当前用户是否具备所需要的角色。校验通过,则允许访问,否则抛出 AccessDeniedException 异常。 JwtAccessDeniedHandler 返回错误信息,这里没有返回403错误。而是算他访问成功,提示权限不足。具体根据业务调整JwtUserDetails : 其中: 声明一个Spring Security 的User实例,供Spring Security 使用。 roles 是当前用户具备的角色列表
package com.securityjwtdemo.entity.security; import com.securityjwtdemo.entity.ElstRole; 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; import java.util.stream.Collectors; /** * @Data: 2019/10/30 * @Des: */ public class JwtUserDetails implements UserDetails { private Integer id; private String userId; private String userName; private String userPwd; private Short userEnabled; private List<ElstRole> roles; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getUserPwd() { return userPwd; } public void setUserPwd(String userPwd) { this.userPwd = userPwd; } public Short getUserEnabled() { return userEnabled; } public void setUserEnabled(Short userEnabled) { this.userEnabled = userEnabled; } public List<ElstRole> getRoles() { return roles; } public void setRoles(List<ElstRole> roles) { this.roles = roles; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return roles == null ? new ArrayList<SimpleGrantedAuthority>() : roles.stream().map(r ->new SimpleGrantedAuthority(r.getRoleId())).collect(Collectors.toList()); } @Override public String getPassword() { return this.userPwd; } @Override public String getUsername() { return this.userName; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return 1 == userEnabled; } }JwtUserDetailsService : 根据 username 获取当前用户。再次强调,这里的username并不是数据库中user_name,而是能唯一确定用户的字段,这里实际意思是 user_id。在进行用户密码等校验时,会调用 UserDetailsService.loadUserByUsername 方法获取到登陆用户,再进行校验。
package com.securityjwtdemo.service.jwtsecurity; import com.securityjwtdemo.dao.ElstUserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; /** * @Data: 2019/10/30 * @Des: */ @Component public class JwtUserDetailsService implements UserDetailsService { @Autowired private ElstUserMapper elstUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return elstUserMapper.loadUserByUsername(username); } }JwtFilterInvocationSecurityMetadataSource
package com.securityjwtdemo.common.config.security; import com.securityjwtdemo.dao.ElstMenuMapper; import com.securityjwtdemo.entity.info.ElstMenuInfo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import java.util.Collection; import java.util.List; /** * @Data: 2019/10/31 * @Des: 获取有权访问当前url的角色列表 */ @Component public class JwtFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private ElstMenuMapper elstMenuMapper; private AntPathMatcher antPathMatcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) object).getRequestUrl(); List<ElstMenuInfo> allMenu = elstMenuMapper.getAllMenuInfo(); for (ElstMenuInfo menu : allMenu) { if (antPathMatcher.match(menu.getMenuUrl(), requestUrl) && menu.getRoles().size() > 0) { String[] roleIds = menu.getRoles().stream().map(r -> r.getRoleId()).toArray(String[]::new); return SecurityConfig.createList(roleIds); } } // 如果没有匹配,则默认全部可以访问 return SecurityConfig.createList(); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } }JwtUrlAccessDecisionManager : 这个类用来校验当前用户是否具备访问当前路径的角色
package com.securityjwtdemo.common.config.security; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.Collection; import java.util.Iterator; /** * @Data: 2019/10/31 * @Des: 校验当前用户是否具有访问该路径的角色 */ @Component public class JwtUrlAccessDecisionManager implements AccessDecisionManager { /** * * @param authentication 当前用户凭证 -- > JwtTokenFilter中将通过验证的用户保存在Security上下文中, 即传入了这里 * @param object 当前请求路径 * @param configAttributes 当前请求路径所需要的角色列表 -- > 从 JwtFilterInvocationSecurityMetadataSource 返回 * @throws AccessDeniedException * @throws InsufficientAuthenticationException */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { Iterator<ConfigAttribute> iterator = configAttributes.iterator(); while (iterator.hasNext()) { ConfigAttribute ca = iterator.next(); //当前请求需要的权限 String needRole = ca.getAttribute(); if (StringUtils.isEmpty(needRole)) { return; } //当前用户所具有的权限 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("权限不足!"); } @Override public boolean supports(ConfigAttribute attribute) { return false; } @Override public boolean supports(Class<?> clazz) { return false; } }ElstUserMapper.xml : 编写了获取用户角色的逻辑
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.securityjwtdemo.dao.ElstUserMapper"> <resultMap id="BaseResultMap" type="com.securityjwtdemo.entity.ElstUser"> <id column="id" jdbcType="INTEGER" property="id"/> <result column="user_id" jdbcType="VARCHAR" property="userId"/> <result column="user_name" jdbcType="VARCHAR" property="userName"/> <result column="user_pwd" jdbcType="VARCHAR" property="userPwd"/> <result column="user_enabled" jdbcType="SMALLINT" property="userEnabled"/> </resultMap> <sql id="Base_Column_List"> id, user_id, user_name, user_pwd, user_enabled </sql> <resultMap id="loadUserByUsernameResultMap" type="com.securityjwtdemo.entity.security.JwtUserDetails"> <result column="user_id" property="userId"></result> <collection property="roles" select="com.securityjwtdemo.dao.ElstRoleMapper.selectRoleByUser" column="user_id" ofType="com.securityjwtdemo.entity.ElstRole"/> </resultMap> <select id="loadUserByUsername" resultMap="loadUserByUsernameResultMap"> select <include refid="Base_Column_List"/> from elst_user where user_id = #{userId} </select> </mapper>ElstMenuMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.securityjwtdemo.dao.ElstMenuMapper"> <resultMap id="BaseResultMap" type="com.securityjwtdemo.entity.ElstMenu"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="menu_id" jdbcType="VARCHAR" property="menuId" /> <result column="menu_name" jdbcType="VARCHAR" property="menuName" /> <result column="parent_id" jdbcType="VARCHAR" property="parentId" /> <result column="menu_url" jdbcType="VARCHAR" property="menuUrl" /> <result column="menu_path" jdbcType="VARCHAR" property="menuPath" /> <result column="menu_enabled" jdbcType="SMALLINT" property="menuEnabled" /> </resultMap> <sql id="Base_Column_List"> id, menu_id, menu_name, parent_id, menu_url, menu_path, menu_enabled </sql> <resultMap id="Menu_Role_Info" type="com.securityjwtdemo.entity.info.ElstMenuInfo"> <collection property="roles" select="com.securityjwtdemo.dao.ElstRoleMapper.selectRoleByMenu" column="menu_id" ofType="com.securityjwtdemo.entity.ElstRole" ></collection> </resultMap> <select id="getAllMenuInfo" resultMap="Menu_Role_Info"> select <include refid="Base_Column_List"></include> from elst_menu </select> </mapper>ElstRoleMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.securityjwtdemo.dao.ElstRoleMapper"> <resultMap id="BaseResultMap" type="com.securityjwtdemo.entity.ElstRole"> <id column="id" jdbcType="INTEGER" property="id" /> <result column="role_id" jdbcType="VARCHAR" property="roleId" /> <result column="role_name" jdbcType="VARCHAR" property="roleName" /> <result column="zh_name" jdbcType="VARCHAR" property="zhName" /> </resultMap> <sql id="Base_Column_List"> id, role_id, role_name, zh_name </sql> <select id="selectRoleByUser" resultType="com.securityjwtdemo.entity.ElstRole"> select r.id, r.role_id, r.role_name, r.zh_name from elst_role r LEFT JOIN elst_user_role ur ON ur.role_id = r.role_id WHERE ur.user_id = #{userId} </select> <select id="selectRoleByMenu" resultType="com.securityjwtdemo.entity.ElstRole"> select r.id, r.role_id, r.role_name, r.zh_name from elst_role r LEFT JOIN elst_role_menu rm ON rm.role_id = r.role_id WHERE rm.menu_id = #{menuId} </select> </mapper>JwtLoginToken : 自定义 Token 类,保存当前用户的信息
package com.securityjwtdemo.entity.security; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; /** * @Data: 2019/10/30 * @Des: 用户鉴权 : 保存当前用户的认证信息,如认证状态,用户名密码,拥有的权限等 */ public class JwtLoginToken extends AbstractAuthenticationToken { /**登录用户信息*/ private final Object principal; /**密码*/ private Object credentials; /**创建一个未认证的授权令牌, * 这时传入的principal是用户名 * */ public JwtLoginToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /**创建一个已认证的授权令牌,如注释中说的那样,这个方法应该由AuthenticationProvider来调用 * 也就是我们写的JwtAuthenticationProvider,有它完成认证后再调用这个方法, * 这时传入的principal为从userService中查出的UserDetails * @param principal * @param credentials * @param authorities */ public JwtLoginToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } }JwtLoginFilter : 登陆拦截器,用户进行登录信息校验
package com.securityjwtdemo.filter.security; import com.securityjwtdemo.entity.security.JwtLoginToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.WebAuthenticationDetails; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Data: 2019/10/30 * @Des: 用户登录验证拦截器 -- 执行顺序在UsernamePasswordAuthenticationFilter 拦截器后 */ public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { /** * 拦截逻辑 * * @param request * @param response * @return * @throws AuthenticationException * @throws IOException * @throws ServletException */ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String userName = request.getParameter("username"); String password = request.getParameter("password"); //创建未认证的凭证(etAuthenticated(false)),注意此时凭证中的主体principal为用户名 JwtLoginToken jwtLoginToken = new JwtLoginToken(userName, password); //将认证详情(ip,sessionId)写到凭证 jwtLoginToken.setDetails(new WebAuthenticationDetails(request)); //AuthenticationManager获取受支持的AuthenticationProvider(这里也就是JwtAuthenticationProvider), //生成已认证的凭证,此时凭证中的主体为userDetails --- 这里会委托给AuthenticationProvider实现类来验证 // 即 跳转到 JwtAuthenticationProvider.authenticate 方法中认证 Authentication authenticatedToken = this.getAuthenticationManager().authenticate(jwtLoginToken); return authenticatedToken; } }JwtAuthenticationProvider : 自定义的用户逻辑校验,在这里进行了用户的信息校验。
package com.securityjwtdemo.common.config.security; import com.securityjwtdemo.entity.security.JwtLoginToken; import org.springframework.security.authentication.*; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; /** * @Data: 2019/10/30 * @Des: 用户角色校验具体实现 */ public class JwtAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; private PasswordEncoder passwordEncoder; public JwtAuthenticationProvider(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { this.userDetailsService = userDetailsService; this.passwordEncoder = passwordEncoder; } /** * 鉴权具体逻辑 * * @param authentication * @return * @throws AuthenticationException */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getName()); //转换authentication 认证时会先调用support方法,受支持才会调用,所以能强转 JwtLoginToken jwtLoginToken = (JwtLoginToken) authentication; defaultCheck(userDetails); // 用户名密码校验 具体逻辑 additionalAuthenticationChecks(userDetails, jwtLoginToken); //构造已认证的authentication JwtLoginToken authenticatedToken = new JwtLoginToken(userDetails, jwtLoginToken.getCredentials(), userDetails.getAuthorities()); authenticatedToken.setDetails(jwtLoginToken.getDetails()); return authenticatedToken; } /** * 是否支持当前类 * * @param authentication * @return */ public boolean supports(Class<?> authentication) { return (JwtLoginToken.class .isAssignableFrom(authentication)); } /** * 一些默认信息的检查 * * @param user */ private void defaultCheck(UserDetails user) { if (!user.isAccountNonLocked()) { throw new LockedException("User account is locked"); } if (!user.isEnabled()) { throw new DisabledException("User is disabled"); } if (!user.isAccountNonExpired()) { throw new AccountExpiredException("User account has expired"); } } /** * 检查密码是否正确 * * @param userDetails * @param authentication * @throws AuthenticationException */ private void additionalAuthenticationChecks(UserDetails userDetails, JwtLoginToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { throw new BadCredentialsException("Bad credentials"); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { throw new BadCredentialsException("Bad credentials"); } } }JwtTokenFilter : token有效性校验的拦截器
package com.securityjwtdemo.filter.security; import com.alibaba.fastjson.JSON; import com.securityjwtdemo.common.JsonResponseStatus; import com.securityjwtdemo.common.JsonResult; import com.securityjwtdemo.entity.security.JwtLoginToken; import com.securityjwtdemo.entity.security.JwtUserDetails; import com.securityjwtdemo.utils.JwtUtils; import io.jsonwebtoken.Claims; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Data: 2019/10/30 * @Des: Token有效性验证拦截器 */ public class JwtTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { try { String token = httpServletRequest.getHeader("Authentication"); if (StringUtils.isEmpty(token)) { httpServletResponse.setContentType("application/json;charset=UTF-8"); JsonResult<String> jsonResult = new JsonResult<>(); jsonResult.setFail(JsonResponseStatus.TokenFail.getCode(), "未登录"); httpServletResponse.getWriter().write(JSON.toJSONString(jsonResult)); return; } Claims claims = JwtUtils.parseJWT(token); if (JwtUtils.isTokenExpired(claims)) { httpServletResponse.setContentType("application/json;charset=UTF-8"); JsonResult<String> jsonResult = new JsonResult<>(); jsonResult.setFail(JsonResponseStatus.TokenFail.getCode(), "登陆失效,请重新登陆"); httpServletResponse.getWriter().write(JSON.toJSONString(jsonResult)); return; } JwtUserDetails user = JSON.parseObject(claims.get("userDetails", String.class), JwtUserDetails.class); JwtLoginToken jwtLoginToken = new JwtLoginToken(user, "", user.getAuthorities()); jwtLoginToken.setDetails(new WebAuthenticationDetails(httpServletRequest)); SecurityContextHolder.getContext().setAuthentication(jwtLoginToken); filterChain.doFilter(httpServletRequest, httpServletResponse); } catch (Exception e) { throw new BadCredentialsException("登陆凭证失效,请重新登陆"); } } }JwtLoginSuccessHandler 登陆验证成功后进入这里,生成token并返回。
package com.securityjwtdemo.service.jwtsecurity; import com.alibaba.fastjson.JSON; import com.securityjwtdemo.common.Constants; import com.securityjwtdemo.common.JsonResult; import com.securityjwtdemo.entity.security.JwtUserDetails; import com.securityjwtdemo.utils.JwtUtils; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Data: 2019/10/30 * @Des: 登陆验证成功处理 */ @Component public class JwtLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { response.setContentType("application/json;charset=UTF-8"); JwtUserDetails jwtUserDetails = (JwtUserDetails) authentication.getPrincipal(); String json = JSON.toJSONString(jwtUserDetails); String jwtToken = JwtUtils.createJwtToken(json, Constants.DEFAULT_TOKEN_TIME_MS); //签发token JsonResult<String> jsonResult = new JsonResult<>(); jsonResult.setSuccess(jwtToken); response.getWriter().write(JSON.toJSONString(jsonResult)); } }JwtLoginFailureHandler : 登陆验证失败后会跳到这里
package com.securityjwtdemo.service.jwtsecurity; import com.alibaba.fastjson.JSON; import com.securityjwtdemo.common.JsonResponseStatus; import com.securityjwtdemo.common.JsonResult; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Data : 2019/10/31 * @Des : 登陆验证失败处理 */ @Component public class JwtLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String msg = "登陆失败"; if (exception instanceof BadCredentialsException || exception instanceof UsernameNotFoundException) { msg = "账户名或者密码输入错误!"; } else if (exception instanceof LockedException) { msg = "账户被锁定,请联系管理员!"; } else if (exception instanceof CredentialsExpiredException) { msg = "密码过期,请联系管理员!"; } else if (exception instanceof AccountExpiredException) { msg = "账户过期,请联系管理员!"; } else if (exception instanceof DisabledException) { msg = "账户被禁用,请联系管理员!"; } JsonResult<String> jsonResult = new JsonResult<>(); jsonResult.setFail(JsonResponseStatus.LoginError.getCode(), msg); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(jsonResult)); } }JwtUtils : jwt 工具类
package com.securityjwtdemo.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import java.security.Key; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @author: GYB * createAt: 2018/9/14 */ @Component public class JwtUtils { private Logger logger = LoggerFactory.getLogger(this.getClass()); public static final long DEFAULT_TOKEN_TIME_MS = 30 * 60 * 1000; /* iss: 该JWT的签发者 sub: 该JWT所面向的用户 aud: 接收该JWT的一方 exp(expires): 什么时候过期,这里是一个Unix时间戳 iat(issued at): 在什么时候签发的 */ /** * 签名秘钥 */ public static final String SECRET = "token"; /** * 生成token * * @param id 一般传入userName * @return */ public static String createJwtToken(String id) { String issuer = "GYB"; String subject = ""; return createJwtToken(id, issuer, subject, DEFAULT_TOKEN_TIME_MS); } public static String createJwtToken(String id, long ttlMillis) { String issuer = "GYB"; String subject = ""; return createJwtToken(id, issuer, subject, ttlMillis); } /** * 生成Token * * @param id 编号 * @param issuer 该JWT的签发者,是否使用是可选的 * @param subject 该JWT所面向的用户,是否使用是可选的; * @param ttlMillis 签发时间 * @return token String */ public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) { // 签名算法 ,将对token进行签名 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成签发时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); // 通过秘钥签名JWT byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET); Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) Map<String, Object> claims = new HashMap<String, Object>(); claims.put("userDetails", id); // Let's set the JWT Claims JwtBuilder builder = Jwts.builder().setId(id) .setIssuedAt(now) .setSubject(subject) .setIssuer(issuer) .setClaims(claims) .signWith(signatureAlgorithm, signingKey); // if it has been specified, let's add the expiration if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); } // Builds the JWT and serializes it to a compact, URL-safe string return builder.compact(); } // Sample method to validate and read the JWT public static Claims parseJWT(String jwt) { // This line will throw an exception if it is not a signed JWS (as expected) try { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET)) .parseClaimsJws(jwt).getBody(); return claims; } catch (Exception exception) { return null; } } /** * 验证jwt的有效期 * * @param claims * @return */ public static Boolean isTokenExpired(Claims claims) { return claims == null || claims.getExpiration().before(new Date()); } }JwtAccessDeniedHandler
package com.securityjwtdemo.service.jwtsecurity; import com.alibaba.fastjson.JSON; import com.securityjwtdemo.common.JsonResponseStatus; import com.securityjwtdemo.common.JsonResult; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Data: 2019/10/31 * @Des: 权限不足异常处理 */ @Component public class JwtAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { JsonResult<String> jsonResult = new JsonResult<>(); jsonResult.setFail(JsonResponseStatus.NoRight.getCode(), "权限不足,请联系管理员 : " + accessDeniedException.getMessage()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(jsonResult)); } }JwtAuthenticationEntryPoint
package com.securityjwtdemo.service.jwtsecurity; import com.alibaba.fastjson.JSON; import com.securityjwtdemo.common.JsonResponseStatus; import com.securityjwtdemo.common.JsonResult; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Data: 2019/10/31 * @Des: 用户权限不足处理 */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { JsonResult<String> jsonResult = new JsonResult<>(); jsonResult.setFail(JsonResponseStatus.NoRight.getCode(), "权限不足 :" + authException.getMessage()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(JSON.toJSONString(jsonResult)); } }2. 张三(userid为1),则都可以访问 3. Token 在十分钟后失效
以上:内容部分参考: https://www.jianshu.com/p/d5ce890c67f7 https://www.jianshu.com/p/fc56d965e3c3 https://www.jianshu.com/p/f987847cdbe3 https://www.cnblogs.com/HHR-SUN/p/7095720.html https://segmentfault.com/a/1190000012763317 https://blog.csdn.net/dushiwodecuo/article/details/78913113 如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正