项目实现了Spring Security 权限控制 + Jwt Token认证。html
类名 | 概念 |
---|---|
AuthenticationManager | 用户认证的管理类,全部的认证请求(好比login)都会经过提交一个token给AuthenticationManager的authenticate()方法来实现。固然事情确定不是它来作,具体校验动做会由AuthenticationManager将请求转发给具体的实现类来作。根据实现反馈的结果再调用具体的Handler来给用户以反馈。 |
AuthenticationProvider | 认证的具体实现类,一个provider是一种认证方式的实现,好比提交的用户名密码我是经过和DB中查出的user记录作比对实现的,那就有一个DaoProvider;若是我是经过CAS请求单点登陆系统实现,那就有一个CASProvider。按照Spring一向的做风,主流的认证方式它都已经提供了默认实现,好比DAO、LDAP、CAS、OAuth2等。前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来作的。一个AuthenticationManager能够包含多个Provider,每一个provider经过实现一个support方法来表示本身支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。 |
UserDetailService | 用户认证经过Provider来作,因此Provider须要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。 |
AuthenticationToken | 全部提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,好比最容易理解的UsernamePasswordAuthenticationToken。 |
SecurityContext | 当用户经过认证以后,就会为这个用户生成一个惟一的SecurityContext,里面包含用户的认证信息Authentication。经过SecurityContext咱们能够获取到用户的标识Principle和受权信息GrantedAuthrity。在系统的任何地方只要经过SecurityHolder.getSecruityContext()就能够获取到SecurityContext。 |
拦截器 | 释义 |
---|---|
HttpSessionContextIntegrationFilter | 位于过滤器顶端,第一个起做用的过滤器。用途一,在执行其余过滤器以前,率先判断用户的session中是否已经存在一个SecurityContext了。若是存在,就把SecurityContext拿出来,放到SecurityContextHolder中,供Spring Security的其余部分使用。若是不存在,就建立一个SecurityContext出来,仍是放到SecurityContextHolder中,供Spring Security的其余部分使用。用途二,在全部过滤器执行完毕后,清空SecurityContextHolder,由于SecurityContextHolder是基于ThreadLocal的,若是在操做完成后清空ThreadLocal,会受到服务器的线程池机制的影响。 |
LogoutFilter | 只处理注销请求,默认为/j_spring_security_logout。用途是在用户发送注销请求时,销毁用户session,清空SecurityContextHolder,而后重定向到注销成功页面。能够与rememberMe之类的机制结合,在注销的同时清空用户cookie。 |
AuthenticationProcessingFilter | 处理form登录的过滤器,与form登录有关的全部操做都是在此进行的。默认状况下只处理/j_spring_security_check请求,这个请求应该是用户使用form登录后的提交地址此过滤器执行的基本操做时,经过用户名和密码判断用户是否有效,若是登陆成功就跳转到成功页面(多是登录以前访问的受保护页面,也多是默认的成功页面),若是登陆失败,就跳转到失败页面。 |
DefaultLoginPageGeneratingFilter | 此过滤器用来生成一个默认的登陆页面,默认的访问地址为/spring_security_login,这个默认的登陆页面虽然支持用户输入用户名,密码,也支持rememberMe功能,可是由于太难看了,只能是在演示时作个样子,不可能直接用在实际项目中。 |
BasicProcessingFilter | 此过滤器用于进行basic验证,功能与AuthenticationProcessingFilter相似,只是验证的方式不一样。 |
SecurityContextHolderAwareRequestFilter | 此过滤器用来包装客户的请求。目的是在原始请求的基础上,为后续程序提供一些额外的数据。好比getRemoteUser()时直接返回当前登录的用户名之类的。 |
RememberMeProcessingFilter | 此过滤器实现RememberMe功能,当用户cookie中存在rememberMe的标记,此过滤器会根据标记自动实现用户登录,并建立SecurityContext,授予对应的权限。 |
AnonymousProcessingFilter | 为了保证操做统一性,当用户没有登录时,默认为用户分配匿名用户的权限。 |
ExceptionTranslationFilter | 此过滤器的做用是处理中FilterSecurityInterceptor抛出的异常,而后将请求重定向到对应页面,或返回对应的响应错误代码 |
SessionFixationProtectionFilter | 防护会话伪造攻击。有关防护会话伪造的详细信息 |
FilterSecurityInterceptor | 用户的权限控制都包含在这个过滤器中。功能一:若是用户还没有登录,则抛出AuthenticationCredentialsNotFoundException“还没有认证异常”。功能二:若是用户已登陆,可是没有访问当前资源的权限,则抛出AccessDeniedException“拒绝访问异常”。功能三:若是用户已登陆,也具备访问当前资源的权限,则放行。咱们能够经过配置方式来自定义拦截规则 |
全部的过滤器都会实现SpringSecurityFilter安全过滤器java
我理解的,就是用户凭证再也不由服务端保存,而是由客户端本身保存。即客户端登录后,将加密登录凭证交于客户端,客户端并不明白凭证有何意义,只知道登录须要使用。在登录访问时咱们获取到登录凭证进行解密,获取到当前用户信息。同时用户凭证隔一段时间会失效。具体介绍能够
https://www.jianshu.com/p/12b609e40029 的介绍git
文末会给出项目地址,因此基础搭建不在赘述。github
1. 登录阶段流程图。
中间省略了Spring Security 的某些调用。仅用来描绘本身代码的逻辑。
2. 登录阶段讲解。(请自觉忽略个人背景图。。。。。)web
1.咱们在配置类中添加了两个自定义拦截器 JwtLoginFilter
和 JwtTokenFilter
。咱们这里关注 JwtLoginFilter
算法
2.JwtLoginFilter
继承了 UsernamePasswordAuthenticationFilter
, UsernamePasswordAuthenticationFilter
是用来处理身份验证的表单提交。也就是说 咱们在 JwtLoginFilter
中处理表单提交的身份信息。spring
咱们进去 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并返回给用户
sql
5.失败则会调用 JwtLoginFailureHandler
返回错误信息
6. 至于为何会调用这两个类,是由于咱们在配置类中进行了初始化配置:
而且在拦截链路中加入了这两个拦截器。
对于添加拦截器如下三个方法:数据库
1. Token验证流程图
2. 代码讲解
1.因为咱们在拦截链中加入了JwtLoginFilter
、JwtTokenFilter
。而 JwtLoginFilter
上面说过只拦截登录路径。其他路径则会被 JwtTokenFilter
拦截。
2. JwtTokenFilter
具体代码以下。
json
JwtFilterInvocationSecurityMetadataSource
中。 JwtFilterInvocationSecurityMetadataSource
根据当前路径获取到有资格访问当前页面的角色列表(好比 Admin,Teacher 等)。JwtUrlAccessDecisionManager
中,在这里来校验当前用户是否具有所须要的角色。校验经过,则容许访问,不然抛出 AccessDeniedException 异常。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)); } }
package com.securityjwtdemo.common.config.security; import com.securityjwtdemo.filter.security.JwtLoginFilter; import com.securityjwtdemo.filter.security.JwtTokenFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.builders.WebSecurity; 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.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; /** * @Data : 2019/10/31 * @Des : Spring Security 配置类 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SpringWebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AccessDeniedHandler accessDeniedHandler; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; @Autowired private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource; @Autowired private AccessDecisionManager accessDecisionManager; @Override protected void configure(HttpSecurity http) throws Exception { // 自定义登录拦截器 JwtLoginFilter jwtLoginFilter = new JwtLoginFilter(); jwtLoginFilter.setAuthenticationManager(authenticationManagerBean()); jwtLoginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); jwtLoginFilter.setAuthenticationFailureHandler(authenticationFailureHandler); JwtTokenFilter jwtTokenFilter = new JwtTokenFilter(); // 使用自定义验证明现器 JwtAuthenticationProvider jwtAuthenticationProvider = new JwtAuthenticationProvider(userDetailsService, passwordEncoder); // 登录验证信息 http.authenticationProvider(jwtAuthenticationProvider) .authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(filterInvocationSecurityMetadataSource); object.setAccessDecisionManager(accessDecisionManager); return object; } }) .anyRequest().authenticated() .and() .formLogin(); // jwt 拦截器配置 http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) //禁用session .and() .csrf().disable() .addFilterAt(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class) // 添加拦截器 .addFilterAfter(jwtTokenFilter, JwtLoginFilter.class); // 权限处理信息 http.exceptionHandling() // 用来解决认证过的用户访问无权限资源时的异常 .accessDeniedHandler(accessDeniedHandler) // 用来解决匿名用户访问无权限资源时的异常 .authenticationEntryPoint(authenticationEntryPoint); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().mvcMatchers("/static/**"); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }
2. 张三(userid为1),则均可以访问
3. Token 在十分钟后失效
以上:内容部分参考:
https://www.jianshu.com/p/d5ce890c67f7
https://www.jianshu.com/p/fc56d965e3c3
https://www.jianshu.com/p/f987847cdbe3
http://www.javashuo.com/article/p-hxvyosau-kd.html
http://www.javashuo.com/article/p-peiemeoh-x.html
http://www.javashuo.com/article/p-rgyffmcv-nn.html
若有侵扰,联系删除。 内容仅用于自我记录学习使用。若有错误,欢迎指正