前言
本章是基于上一章“JWT+SpringSecurity实现基于Token的单点登录(一):前期准备”的基础上进行开发的,如果前期准备还没有做好的,可点击链接至上一章。
代码地址:gitee
一、JWT工具类
这里我们使用jjwt来构建我们的Token。首先导入jjwt的依赖包。
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
接着构建JwtTokenUtils工具类。
package com.shiep.jwtauth.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
/**
* @author: 倪明辉
* @date: 2019/3/6 15:11
* @description: JWT工具类
* JWT是由三段组成的,分别是header(头)、payload(负载)和signature(签名)
* 其中header中放{
* "alg": "HS512",
* "typ": "JWT"
* } 表明使用的加密算法,和token的类型==>默认是JWT
*
*/
public class JwtTokenUtils {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
//密钥,用于signature(签名)部分解密
private static final String PRIMARY_KEY = "jwtsecretdemo";
//签发者
private static final String ISS = "Gent.Ni";
// 添加角色的key
private static final String ROLE_CLAIMS = "role";
// 过期时间是3600秒,既是1个小时
private static final long EXPIRATION = 3600L;
// 选择了记住我之后的过期时间为7天
private static final long EXPIRATION_REMEMBER = 604800L;
/**
* description: 创建Token
*
* @param username
* @param isRememberMe
* @return java.lang.String
*/
public static String createToken(String username, List<String> roles, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, roles);
return Jwts.builder()
//采用HS512算法对JWT进行的签名,PRIMARY_KEY是我们的密钥
.signWith(SignatureAlgorithm.HS512, PRIMARY_KEY)
//设置角色名
.setClaims(map)
//设置发证人
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
/**
* description: 从token中获取用户名
*
* @param token
* @return java.lang.String
*/
public static String getUsername(String token){
return getTokenBody(token).getSubject();
}
// 获取用户角色
public static List<String> getUserRole(String token){
return (List<String>) getTokenBody(token).get(ROLE_CLAIMS);
}
/**
* description: 判断Token是否过期
*
* @param token
* @return boolean
*/
public static boolean isExpiration(String token){
return getTokenBody(token).getExpiration().before(new Date());
}
/**
* description: 获取
*
* @param token
* @return io.jsonwebtoken.Claims
*/
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(PRIMARY_KEY)
.parseClaimsJws(token)
.getBody();
}
}
JwtTokenUtils类的createToken方法,传入参数UserName为用户名,roles是该用户的角色列表,isRememberMe代表是否记住我,从而选择Token的过期时间。getUsername和getUserRole方法分别用来读取Token中的用户名和该用户的角色列表。isExpiration方法用来判断该Token是否过期。
二、实现UserDetails,封装用户信息
package com.shiep.jwtauth.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
/**
* @author: 倪明辉
* @date: 2019/3/6 15:27
* @description: 实现UserDetails,封装用户信息,用于验证身份
*/
public class JwtAuthUser implements UserDetails {
private Integer id;
private String userName;
private String password;
private List<String> roles;
private Collection<? extends GrantedAuthority> authorities;
/**
* description: 通过FXUser来创建JwtAuthUser
*
* @param user
* @return
*/
public JwtAuthUser(FXUser user){
this.id=user.getId();
this.userName=user.getName();
this.password=user.getPassword();
this.roles=user.getRoles();
}
/**
* description: 鉴权最重要方法,通过此方法来返回用户权限
*
* @param
* @return java.util.Collection<? extends org.springframework.security.core.GrantedAuthority>
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new HashSet<>();
if (roles!=null) {
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
System.out.println("authorities:"+authorities);
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@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 true;
}
@Override
public String toString() {
return "JwtAuthUser{" +
"id=" + id +
", username='" + userName + '\'' +
", password='" + password + '\'' +
", authorities=" + roles +
'}';
}
}
JwtAuthUser类实现了UserDetails,从而封装了用户信息,用于认证和鉴权。
三、实现UserDetailsService,从数据库加载用户信息(UserDetails)
package com.shiep.jwtauth.service.impl;
import com.shiep.jwtauth.entity.FXUser;
import com.shiep.jwtauth.entity.JwtAuthUser;
import com.shiep.jwtauth.repository.FXUserRepository;
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.Service;
/**
* @author: 倪明辉
* @date: 2019/3/6 16:26
* @description: 实现UserDetailsService,从数据库中加载用户信息==》用户名、密码及角色名
*
* Spring Security中进行身份验证的是AuthenticationManager接口,ProviderManager是它的一个默认实现,但它并不用来处理身份认证,
* 而是委托给配置好的AuthenticationProvider,每个AuthenticationProvider会轮流检查身份认证。检查后或者返回Authentication对象或者抛出异常。
*
* 验证身份就是加载响应的UserDetails,看看是否和用户输入的账号、密码、权限等信息匹配。
* 此步骤由实现AuthenticationProvider的DaoAuthenticationProvider(它利用UserDetailsService验证用户名、密码和授权)处理。
* 包含 GrantedAuthority 的 UserDetails对象在构建 Authentication对象时填入数据。
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private FXUserRepository userRepository;
/**
* description: 通过用户名从数据库中读取该用户账户信息及权限信息
*
* @param userName 用户名
* @return org.springframework.security.core.userdetails.UserDetails
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
FXUser user = userRepository.findByName(userName);
if(user==null){
// 实际当用户不存在时,应该页面显示错误信息,并跳转到登录界面
throw new UsernameNotFoundException("该用户不存在!");
}
user.setRoles(userRepository.getRolesByUserName(userName));
System.out.println("UserDetailsServiceImpl==>loadUserByUsername:"+user.toString());
return new JwtAuthUser(user);
}
}
UserDetailsServiceImpl实现了UserDetailsService,只有一个方法loadUserByUsername,通过查询用户名从数据库中加载用户信息(UserDetails),这里是JwtAuthUser。
为了更清晰理解SpringSecurity的认证鉴权原理,下面讲解下SpringSecurity中一些核心类。
- AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给
AuthenticationManager
的authenticate()
方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager
将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。 - AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个
DaoProvider
;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider
。
前面讲了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
。
这里先大概了解下,下面我们接着Coding。
四、配置登录校验拦截器
在配置登录校验拦截器前,我们一般先创建一个Model类,用来接收用户登录信息。
package com.shiep.jwtauth.model;
import lombok.Data;
/**
* @author: 倪明辉
* @date: 2019/3/6 16:18
* @description: 封装了用户登录时的信息
*/
@Data
public class LoginUser {
private String username;
private String password;
private Boolean rememberMe;
}
LoginUser类中有三个字段,用户名、密码、是否记住我。实际开发时还能加入验证码等。
配置好了LoginUser类后,我们接着来看看如何配置过滤器。
package com.shiep.jwtauth.filter;
import com.alibaba.fastjson.JSON;
import com.shiep.jwtauth.common.ResultEnum;
import com.shiep.jwtauth.common.ResultVO;
import com.shiep.jwtauth.entity.JwtAuthUser;
import com.shiep.jwtauth.model.LoginUser;
import com.shiep.jwtauth.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
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.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* @author: 倪明辉
* @date: 2019/3/6 16:12
* @description: 进行用户账号的验证==>认证功能
*
*/
public class JwtLoginAuthFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private ThreadLocal<Boolean> rememberMe = new ThreadLocal<>();
public JwtLoginAuthFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
// 设置该过滤器地址
super.setFilterProcessesUrl("/auth/login");
}
/**
* description: 登录验证
*
* @param request
* @param response
* @return org.springframework.security.core.Authentication
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
LoginUser loginUser = new LoginUser();
loginUser.setUsername(request.getParameter("username"));
loginUser.setPassword(request.getParameter("password"));
loginUser.setRememberMe(Boolean.parseBoolean(request.getParameter("rememberMe")));
System.out.println(loginUser.toString());
rememberMe.set(loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
}
/**
* description: 登录验证成功后调用,验证成功后将生成Token,并重定向到用户主页home
* 与AuthenticationSuccessHandler作用相同
*
* @param request
* @param response
* @param chain
* @param authResult
* @return void
*/
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象,这里是JwtAuthUser
JwtAuthUser jwtUser = (JwtAuthUser) authResult.getPrincipal();
System.out.println("JwtAuthUser:" + jwtUser.toString());
boolean isRemember = rememberMe.get();
List<String> roles = new ArrayList<>();
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
roles.add(authority.getAuthority());
}
System.out.println("roles:"+roles);
String token = JwtTokenUtils.createToken(jwtUser.getUsername(), roles,isRemember);
System.out.println("token:"+token);
// 重定向无法设置header,这里设置header只能设置到/auth/login界面的header
//response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
// 登录成功重定向到home界面
// 这里先采用参数传递
response.sendRedirect("/home?token="+token);
}
/**
* description: 登录验证失败后调用,这里直接Json返回,实际上可以重定向到错误界面等
* 与AuthenticationFailureHandler作用相同
*
* @param request
* @param response
* @param failed
* @return void
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(ResultVO.result(ResultEnum.USER_LOGIN_FAILED,false)));
}
}
JwtLoginAuthFilter实现了UsernamePasswordAuthenticationFilter,该拦截器的attemptAuthentication方法,通过request.getParameter方法来得到前端传来的登录参数,并构建LoginUser对象。PS:从代码看LoginUser是不是有点多余?我们是不是可以直接得到参数进行校验?嗯……是可以
的,但是原本正确的思路是通过从输入流中直接构建登录对象的,代码如下:
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
但是LoginUser的Boolean rememberMe字段,老是无法与前端数据进行匹对(将Boolean改成String也报错,错误信息如下),因此这里先采用这种方式读取数据吧。ps:有知道如何解决的小伙伴,麻烦告诉我下,感激不尽~
com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
我们接着看attemptAuthentication方法,该方法从登录界面读取到数据后,通过authenticationManager.authenticate方法,让SpringSecurity去进行验证,不需要自己查数据库对用户名和密码进行配对。
attemptAuthentication方法进行校验后,有两种结果:成功或失败。当验证成功时将调用successfulAuthentication方法,失败调用unsuccessfulAuthentication方法。
successfulAuthentication方法中首先通过Authentication.getPrincipal()方法来得到当前用户的信息(UserDetails),接着通过用户名、角色列表和是否记住我来构建Token。原本应该Token将放在response的header中的,但是设置header只能设置在当前页面的response中?(有知道如何设置重定向后页面的header的小伙伴,麻烦告诉我下,感激不尽~(/ □ \))至于为什么要页面重定向先卖个关子,我们先往下看。
unsuccessfulAuthentication方法是用户认证失败后返回信息,这里用到了两个工具类,下面我们先来看看他们。
五、response状态码
package com.shiep.jwtauth.common;
import lombok.Getter;
/**
* @author: 倪明辉
* @date: 2019/3/7 17:13
* @description: JWT认证==》认证结果的枚举类
*/
@Getter
public enum ResultEnum {
/**
* description: 认证结果状态码及信息
*
* @param null
* @return
*/
SUCCESS(101,"成功"),
FAILURE(102,"失败"),
USER_NEED_AUTHORITIES(201,"用户未登录"),
USER_LOGIN_FAILED(202,"用户账号或密码错误"),
USER_LOGIN_SUCCESS(203,"用户登录成功"),
USER_NO_ACCESS(204,"用户无权访问"),
USER_LOGOUT_SUCCESS(205,"用户登出成功"),
TOKEN_IS_BLACKLIST(206,"此token为黑名单"),
LOGIN_IS_OVERDUE(207,"登录已失效"),
;
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
/**
* description: 通过code返回message
*
* @param code
* @return com.shiep.jwtauth.common.ResultEnum
*/
public static ResultEnum parse(int code){
ResultEnum[] values = values();
for (ResultEnum value : values) {
if(value.getCode() == code){
return value;
}
}
throw new RuntimeException("Unknown code of ResultEnum");
}
}
ResultEnum封装一些常用的状态码。
package com.shiep.jwtauth.common;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @author: 倪明辉
* @date: 2019/3/7 17:23
* @description: response返回结果集
*/
public class ResultVO implements Serializable {
private static final long serialVersionUID = -5359028332240046810L;
/**
* description: 返回响应信息
*
* @param respCode
* @param success
* @return java.util.Map<java.lang.String,java.lang.Object>
*/
public static Map<String, Object> result(ResultEnum respCode, Boolean success) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("code", respCode.getCode());
map.put("message", respCode.getMessage());
map.put("data", null);
map.put("success",success);
return map;
}
/**
* description: 返回响应信息及Token
*
* @param respCode
* @param jwtToken
* @param success
* @return java.util.Map<java.lang.String,java.lang.Object>
*/
public final static Map<String, Object> result(ResultEnum respCode, String jwtToken, Boolean success) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("jwtToken",jwtToken);
map.put("code", respCode.getCode());
map.put("message", respCode.getMessage());
map.put("data", null);
map.put("success",success);
return map;
}
}
ResultVO用于返回结果集。
六、BasicAuthenticationFilter过滤器
package com.shiep.jwtauth.filter;
import com.shiep.jwtauth.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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.Collection;
import java.util.HashSet;
import java.util.List;
/**
* @author: 倪明辉
* @date: 2019/3/6 16:20
* @description: 对所有请求进行过滤
* BasicAuthenticationFilter继承于OncePerRequestFilter==》确保在一次请求只通过一次filter,而不需要重复执行。
*/
public class JwtPreAuthFilter extends BasicAuthenticationFilter {
public JwtPreAuthFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
/**
* description: 从request的header部分读取Token
*
* @param request
* @param response
* @param chain
* @return void
*/
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("BasicAuthenticationFilters");
String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
System.out.println("tokenHeader:"+tokenHeader);
// 如果请求头中没有Authorization信息则直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
super.doFilterInternal(request, response, chain);
}
/**
* description: 读取Token信息,创建UsernamePasswordAuthenticationToken对象
*
* @param tokenHeader
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
//解析Token时将“Bearer ”前缀去掉
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
String username = JwtTokenUtils.getUsername(token);
List<String> roles = JwtTokenUtils.getUserRole(token);
Collection<GrantedAuthority> authorities = new HashSet<>();
if (roles!=null) {
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
}
if (username != null){
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
return null;
}
}
在JwtLoginAuthFilter中我们对登录的用户进行认证,认证成功时将生成Token给用户前端,之后前端保存该Token在Cookie或session中,当用户访问服务器时,只需携带该Token即可访问服务。而Token的认证鉴权工作就是由本例中的JwtPreAuthFilter来实现了。
JwtPreAuthFilter继承了BasicAuthenticationFilter,该过滤器是继承于OncePerRequestFilter,用于确保在一次请求只通过一次filter,而不需要重复执行。简单的说,就是用户的每次请求都将经过该过滤器。下面我们详细看看该过滤器中的代码。
doFilterInternal方法从request的header中查看是否带有Token,如果没有则放行,如果有则进行Token解析(调用getAuthentication方法),并设置认证信息。
七、配置Handler
package com.shiep.jwtauth.handler;
import com.alibaba.fastjson.JSON;
import com.shiep.jwtauth.common.ResultEnum;
import com.shiep.jwtauth.common.ResultVO;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: 倪明辉
* @date: 2019/3/8 9:44
* @description: 用户登出成功时返回给前端的数据
*/
public class FxLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.getWriter().write(JSON.toJSONString(ResultVO.result(ResultEnum.USER_LOGOUT_SUCCESS,true)));
}
}
package com.shiep.jwtauth.handler;
import com.alibaba.fastjson.JSON;
import com.shiep.jwtauth.common.ResultEnum;
import com.shiep.jwtauth.common.ResultVO;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author: 倪明辉
* @date: 2019/3/7 15:18
* @description: 用户未登录时返回给前端的数据
*/
public class UnAuthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
// response.setStatus(HttpServletResponse.SC_FORBIDDEN);
// String reason = "统一处理,原因:"+authException.getMessage();
// response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
response.getWriter().write(JSON.toJSONString(ResultVO.result(ResultEnum.USER_NEED_AUTHORITIES,false)));
}
}
FxLogoutSuccessHandler是用户成功登出时调用的,而UnAuthorizedEntryPoint是用户未登录时调用的。Spring中handle还有许多,不一一列举了。
八、配置SpringSecurity
到这里基本操作都写好啦,现在就需要我们将这些辛苦写好的“组件”组合到一起发挥作用了,那就需要配置SpringSecurity了。
package com.shiep.jwtauth.config;
import com.shiep.jwtauth.filter.JwtLoginAuthFilter;
import com.shiep.jwtauth.filter.JwtPreAuthFilter;
import com.shiep.jwtauth.handler.FxLogoutSuccessHandler;
import com.shiep.jwtauth.handler.UnAuthorizedEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
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.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* @author: 倪明辉
* @date: 2019/3/6 16:24
* @description:
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
// 因为UserDetailsService的实现类实在太多啦,这里设置一下我们要注入的实现类
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
// 加密器
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* description: 加载userDetailsService,用于从数据库中取用户信息
*
* @param auth
* @return void
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
/**
* description: http细节
*
* @param http
* @return void
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 开启跨域资源共享
http.cors()
.and()
// 关闭csrf
.csrf().disable()
// 关闭session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic().authenticationEntryPoint(new UnAuthorizedEntryPoint())
.and()
.authorizeRequests()
// 需要角色为ADMIN才能删除该资源
.antMatchers(HttpMethod.DELETE,"/tasks/**").hasAnyRole("ADMIN")
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
//.successHandler(new Fx)
.and()
.logout()//默认注销行为为logout
.logoutSuccessHandler(new FxLogoutSuccessHandler())
.and()
// 添加到过滤链中
// 先是UsernamePasswordAuthenticationFilter用于login校验
.addFilter(new JwtLoginAuthFilter(authenticationManager()))
// 再通过OncePerRequestFilter,对其他请求过滤
.addFilter(new JwtPreAuthFilter(authenticationManager()));
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
configure(HttpSecurity http)方法是重点,具体在代码注释里面已经解释清楚了。下面我们来配置下Controller和视图。
注意:配置SpringSecurity,需要从细粒度到粗粒度。不然细粒度将不起作用。
九、thymeleaf视图
loginPage.html==》登录界面
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
<script>
function changeValue(){
var check = document.getElementById("rememberMe");
if(check.checked == true){
document.getElementById("rememberMe").value = true;
}else{
document.getElementById("rememberMe").value = false;
}
}
</script>
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<form th:action="@{/auth/login}" method="post">
<div><label> User Name : <input type="text" name="username"/> </label></div>
<div><label> Password: <input type="password" name="password"/> </label></div>
<div><label> RememberMe: <input type="checkbox" name="rememberMe" id="rememberMe" onclick="changeValue()"/></label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
</body>
</html>
loginPage页面,定义了username、password和rememberMe来得到用户登录信息(看前面的登录认证过滤器),接着提交到“/auth/login”路径,该路径就是前面JwtLoginAuthFilter中配置的路径,提交到该过滤器中进行登录验证。
homePage.html==》用户登录验证成功后进入的用户主页
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Home</title>
<script src="jquery-3.1.1.min.js" type="text/javascript"></script>
<script src="getToken.js" type="text/javascript"></script>
</head>
<body>
<script>
var token=$.getUrlParam("token");
window.onload = function () {
$("#token").val(token);
}
function deleteTask() {
$.ajax({
type : 'delete',
url : '/tasks/1',
contentType : 'application/json;charset=UTF-8',
dataType : 'text',
beforeSend: function(request) {
request.setRequestHeader("Authorization","Bearer "+token);
},
success : function(data,textStatus,jqXHR){
alert("response:"+data);
},
error: function (err) {
alert("ajax错误码:" + err.status);
}
});
}
</script>
<h1>Login success</h1>
<textarea id="token" rows="5" cols="50"></textarea>
<input type="button" value="admin角色才能删除" onclick="deleteTask()"/>
</body>
</html>
homePage.html中导入了jquery和自己写的一个getToken.js,用于从URL中得到参数。而按钮的点击事件调用deleteTask()方法,用于发送delete请求到“/tasks”,该路径需要“ROLE_ADMIN”权限。
/*获取到Url里面的参数*/
(function ($) {
$.getUrlParam = function (name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
})(jQuery);
十、Controller控制层
package com.shiep.jwtauth.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @author: 倪明辉
* @date: 2019/3/8 10:31
* @description:
*/
@Controller
public class LoginController {
@GetMapping("/login")
public String toLoginPage(){
return "loginPage";
}
@GetMapping("/home")
public String toHomePage(){
return "homePage";
}
}
LoginController中定义了“/login”路径指向loginPage.html页面,“/home”路径指向homePage.html页面。
package com.shiep.jwtauth.controller;
import org.springframework.web.bind.annotation.*;
/**
* @author: 倪明辉
* @date: 2019/3/6 16:35
* @description:
*/
@RestController
@RequestMapping(path = "/tasks",produces = "application/json;charset=gbk")
public class TaskController {
@GetMapping
public String listTasks(){
return "任务列表";
}
@PostMapping
public String newTasks(){
return "创建了一个新的任务";
}
@PutMapping("/{taskId}")
public String updateTasks(@PathVariable("taskId")Integer id){
return "更新了一下id为:"+id+"的任务";
}
@DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId")Integer id){
return "删除了id为:"+id+"的任务";
}
}
TaskController就是对应SpringSecurity配置类中的需要权限才能访问的路径。
好了,终于开发完成了,接下来我们运行项目来测试下吧。
十一、运行结果测试
首先,我们在数据库中插入两个用户,通过上一章的方法进行插入用户。
用户名:wang,密码:123(数据库中的密码是经过加密后的),该用户具有的权限(ROLE_USER、ROLE_ADMIN)
用户名:li,密码:123(数据库中的密码是经过加密后的),该用户具有的权限(ROLE_USER)
接着使用浏览器访问http://localhost:8080/tasks,即get方式。因为“/tasks/**”路径是需要权限认证的,但是我们此时未验证(在header中设置Token),因此会跳转到http://localhost:8080/login界面。
我们在这个界面使用wang用户登录。页面从“/login”跳转到“/auth/login”路径进行认证,认证成功重定向到“/home”页面,并通过参数形式携带Token到前台。现在我们看看程序的控制台输出:
可以看到认证成功了,Token已经生成。下面是home界面。
当我们点击按钮时:
可以看到权限认证成功,已经执行方法。
接着,我们重新以li用户登录试试,步骤同上。
发现发生403错误,这是用户无权限。
十二、后记
以上关于使用JWT+SpringSecurity实现基于Token的单点登录之认证和授权部分已经完成。但是除了上面提到的,还有一些问题:因为JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。在实际情况下,用户登出时,应该使本次的Token失效。这是其中一个问题。另外关于JWT,我们还能继续跟Redis进行集成,将其缓存到Redis中。最后,跟SpringCloud进行继承,作为Zuul网关的认证入口。这些我们将在下一章继续开发。