续:
Spring Security实现单点登录2_寒風冷度夜雨的博客-CSDN博客
一.Spring Security+Jwt
此前,在处理登录业务的时候,当登录成功时,返回的字符串并不是jwt数据,那么我们就应该将返回的内容改为必要的jwt数据:
@Service
public class AdminServiceImpl implements IAdminService {
@Autowired
private AuthenticationManager authenticationManager;
private String secretKey = "algkjaeihjaglkjfklaf";
@PostMapping("/login")
public String login(AdminLoginDTO adminLoginDTO){
Authentication authentication = new UsernamePasswordAuthenticationToken(
adminLoginDTO.getUsername(),adminLoginDTO.getPassword()
);
Authentication authenticate = authenticationManager.
authenticate(authentication);
// 生成jwt数据
User user = (User) authenticate.getPrincipal();
Map claims = new HashMap<>();
claims.put("username",user.getUsername());
claims.put("permissions",user.getAuthorities());
String jwt = Jwts.builder()
.setHeaderParam(Header.CONTENT_TYPE,"HS256")
.setHeaderParam(Header.TYPE,Header.JWT_TYPE)
.setClaims(claims)
.setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
.signWith(SignatureAlgorithm.HS256,secretKey)
.compact();
return jwt;
}
}
在控制器中,我们为了前端同事能够得到一个统一的返回对象,并不是返回各种类型的对象,那么我们就需要创建一个统一管理返回值对象的类,返回的是一个JSON格式的字符串.
@Data public class JsonResultimplements Serializable { // 状态码,例如:200 private Integer state; // 消息,例如:"登录失败,用户名不存在" private String message; // 数据 private T data; private JsonResult() {} public static JsonResult ok() { return ok(null); } public static JsonResult ok(T data) { JsonResult jsonResult = new JsonResult<>(); jsonResult.setState(State.OK.getValue()); jsonResult.setData(data); return jsonResult; } public static JsonResult fail(State state, String message) { JsonResult jsonResult = new JsonResult<>(); jsonResult.setState(state.getValue()); jsonResult.setMessage(message); return jsonResult; } }
同时,我们还需要在这个类中使用固定的状态码,来表示我们当前返回的是成功或者失败.因此,还需要创建枚举类型,统一管理状态码.
public enum State {
OK(20000),
ERR_BAD_REQUEST(40000), // 客户端引起的--请求参数格式错误
ERR_CATEGORY_NAME_DUPLICATE(40100), // 客户端引起的--类别--名称冲突(被占用)
ERR_CATEGORY_NOT_FOUND(40101), // 客户端引起的--类别--数据不存在(查询参数值不正确)
ERR_JWT_EXPIRED(40900), // 客户端引起的--JWT--过期
ERR_JWT_MALFORMED(40901), // 客户端引起的--JWT--数据无效
ERR_JWT_SIGNATURE(40902), // 客户端引起的--JWT--签名错误
ERR_INSERT(50000), // 服务端引起的--插入数据错误
ERR_UPDATE(50001), // 服务端引起的--更新数据错误
ERR_INTERNAL_SERVER_ERROR(50100); // 服务端引起的--服务器内部错误
private Integer value;
State(Integer value) {
this.value = value;
}
public Integer getValue() {
return value;
}
}
控制器层调用业务逻辑层方法:
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
public class AdminController {
@Autowired
private IAdminService adminService;
@RequestMapping("/login")
public JsonResult login(AdminLoginDTO adminLoginDTO){
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
}
重启项目,在浏览器地址栏中,使用正确的用户名和密码访问,响应结果可以得到一个jwt令牌,这个就是后期前端使用这个令牌保存用户登录数据来访问各个模块.(此处jwt令牌因为在上面设置了过期时间,所以当该令牌过期就需要重新请求!!!)
二.真正的登录流程
以上只是当访问/admins/login时会执行所编写的流程,并不代表是真正意义上的登录.
真正的登录需要浏览器在请求中携带jwt令牌,然后服务器端验证这个jwt,验证成功即视为登录成功.
为了便于体现这个效果,我们可以使用Knife4j,但是需要注意的是,我们使用Knife4j的时候需要在白名单中设置他的路径,以便能够正常响应:
1.添加Knife4j依赖项:
com.github.xiaoymin knife4j-spring-boot-starter2.0.9
2.添加Knife4j配置类:(注意,固定写法,只需修改[指定Controller包路径]即可,其余可以不用修改)
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
private String basePackage = "cn.youle.passport.controller";
private String groupName = "login";
private String host = "http://java.youle.cn";
private String title = "悠乐家电社区平台在线API文档";
private String description = "悠乐家电社区平台在线API文档";
private String termsOfServiceUrl = "http://www.apache.org/licenses/LICENSE-2.0";
private String contactName = "悠乐社区开发部";
private String contactUrl = "http://java.youle.cn";
private String contactEmail = "java@youle.cn";
private String version = "1.0.0";
@Autowired
private OpenApiExtensionResolver openApiExtensionResolver;
@Bean
public Docket docket() {
String groupName = "1.0.0";
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.host(host)
.apiInfo(apiInfo())
.groupName(groupName)
.select()
.apis(RequestHandlerSelectors.basePackage(basePackage))
.paths(PathSelectors.any())
.build()
.extensions(openApiExtensionResolver.buildExtensions(groupName));
return docket;
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title(title)
.description(description)
.termsOfServiceUrl(termsOfServiceUrl)
.contact(new Contact(contactName, contactUrl, contactEmail))
.version(version)
.build();
}
}
3.配置yml文件:
knife4j: enable: true
4.在过滤器白名单中添加路径
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁止跨域攻击
http.csrf().disable();
// 定义白名单
String[] urls = {
"/admins/login",
"doc.html",
"*.js",
"*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
// 配置各请求路径的认证与授权
http.authorizeRequests() // 请求需要授权才可以访问
.antMatchers(urls) // 配置白名单内的请求路径放行
.permitAll() // 允许直接访问
.anyRequest() // 匹配除了以上配置的其他请求
.authenticated(); // 都需要认证
}
}
再次启动项目,输入localhost:8080/doc.html,查看Knife4j是否可以正常显示:
三.创建过滤器
在规范的使用方式中,jwt令牌必须携带在请求头的Authorization属性中
服务器每次在收到请求的时候,应该先判断请求头中是否存在Authorization,以及Authorization的值是否有效等操作,通常使用过滤器来检查.
创建过滤器,并且继承OncePerRequestFilter类
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("=====这是一个过滤器=====");
}
}
需要注意的是,所有过滤器都需要注册之后才可以使用,一个项目可以有多个过滤器,形成过滤器链.
该用于验证Jwt的过滤器应该在Spring Security处理登录的过滤器之前,需要在自定义的配置类中将过滤器注册在相关过滤器之前
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁止跨域攻击
http.csrf().disable();
// 定义白名单
String[] urls = {
"/admins/login",
"/doc.html",
"*.js",
"*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
// 配置各请求路径的认证与授权
http.authorizeRequests() // 请求需要授权才可以访问
.antMatchers(urls) // 配置白名单内的请求路径放行
.permitAll() // 允许直接访问
.anyRequest() // 匹配除了以上配置的其他请求
.authenticated(); // 都需要认证
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
以上步骤完成后,重启项目,在控制台可以看到过滤器中的输出
四.过滤器中的实现:
这个过滤器需要实现的内容有:
1.从请求中获取jwt令牌
2.如果存在jwt令牌,尝试解析
3.将解析得到的数据封装到Authentication对象中
4.为了避免存入一次之后,Spring Context中始终存在Authentication,在过滤器执行的第一时间,就要清除上下文中的数据
还需要添加fastjson依赖项:
com.alibaba
fastjson
1.2.75
public interface JwtConst {
String KEY_USERNAME = "username";
String KEY_PERMISSIONS = "permissions";
}
不添加这个接口,会报JSONException,类型不匹配的异常
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
// 此处的密钥要与生成jwt令牌时的密钥一致
// 密钥和各个Key应该统一定义(后期处理)
private String secretKey = "algkjaeihjaglkjfklaf";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response
, FilterChain filterChain) throws ServletException, IOException {
System.out.println("=====这是一个过滤器=====");
// 4.为了避免存入一次之后,Spring Context中始终存在Authentication,在过滤器执行的第一时间,就要清除上下文中的数据
System.out.println("清除上下文数据");
SecurityContextHolder.clearContext();
// 1.从请求中获取jwt令牌
String jwt = request.getHeader("Authorization");
// 2.如果存在jwt令牌,尝试解析
// 判断是否存在jwt令牌
if(!StringUtils.hasText(jwt)){
// 不存在jwt令牌,则放行,后续还有其它过滤器及相关组件进行其它的处理,例如未登录则要求登录等
// 此处不宜直接阻止运行,因为“登录”、“注册”等请求本应该没有jwt数据
System.out.println("请求头中无jwt数据,当前过滤器将放行");
filterChain.doFilter(request,response);// 继续执行过滤器链中后续的过滤器
return;
}
// 注意:此时执行时,如果请求头中携带了Authentication,日志中将输出,且不会有任何响应,因为当前过滤器尚未放行
// 以下代码有可能抛出异常的
String username = null;
String permissionsString = null;
try{
Claims claims = Jwts.parser().setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
username = claims.get(JwtConst.KEY_USERNAME).toString();
permissionsString = claims.get(JwtConst.KEY_PERMISSIONS).toString();
}catch (ExpiredJwtException e){
System.out.println("解析jwt失败,此jwt已过期");
JsonResult jsonResult = JsonResult.fail(State.ERR_JWT_EXPIRED, "您的登录已过期,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonString);
return;
}catch (MalformedJwtException e){
System.out.println("解析jwt失败,此jwt令牌错误,无法解析");
JsonResult jsonResult = JsonResult.fail(State.ERR_JWT_MALFORMED, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonString);
return;
}catch (SignatureException e){
System.out.println("解析jwt失败,此jwt签名错误");
JsonResult jsonResult = JsonResult.fail(State.ERR_JWT_SIGNATURE, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonString);
return;
}catch (Throwable e){
System.out.println("解析jwt失败,异常类型:" + e.getClass().getName());
JsonResult jsonResult = JsonResult.fail(State.ERR_INTERNAL_SERVER_ERROR, "获取登录信息失败,请重新登录!");
String jsonString = JSON.toJSONString(jsonResult);
response.setContentType("application/json;charset=utf-8");
response.getWriter().println(jsonString);
return;
}
// 3.将解析得到的数据封装到Authentication对象中
// 将此前从JWT中读取到的permissionsString(JSON字符串)转换成Collection extends GrantedAuthority>
List permissions = JSON.parseArray(permissionsString, SimpleGrantedAuthority.class);
System.out.println("从JWT中获取到的权限转换成Spring Security要求的类型:" + permissions);
// 将解析得到的用户信息传递给Spring Security
// 在Authentication中封装:用户名、null(密码)、权限列表
// 因为接下来并不会处理认证,所以Authentication中不需要密码
// 后续,Spring Security发现上下文中有Authentication时,就会视为已登录,甚至可以获取相关信息
Authentication authentication = new UsernamePasswordAuthenticationToken
(username,null,permissions);
// 获取Spring Security的上下文,并将Authentication放到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);
// 放行
filterChain.doFilter(request,response);
}
}
需要在配置类上使用@EnableGlobalMethodSecurity注解开启“通过注解配置权限”的功能
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁止跨域攻击
http.csrf().disable();
// 定义白名单
String[] urls = {
"/admins/login",
"/doc.html",
"*.js",
"*.css",
"/swagger-resources",
"/v2/api-docs",
"/favicon.ico"
};
// 配置各请求路径的认证与授权
http.authorizeRequests() // 请求需要授权才可以访问
.antMatchers(urls) // 配置白名单内的请求路径放行
.permitAll() // 允许直接访问
.anyRequest() // 匹配除了以上配置的其他请求
.authenticated(); // 都需要认证
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
最后,在任何你需要设置权限的处理请求的方法上,通过@PreAuthorize注解来配置要求某种权限
@RestController
@RequestMapping(value = "/admins", produces = "application/json; charset=utf-8")
@Api(tags = "用户登录模块")
public class AdminController {
@Autowired
private IAdminService adminService;
@PostMapping("/login")
@ApiOperation(value = "登录")
public JsonResult login(AdminLoginDTO adminLoginDTO){
String jwt = adminService.login(adminLoginDTO);
return JsonResult.ok(jwt);
}
@GetMapping("/hello")
@ApiOperation(value = "测试能否登录成功")
@PreAuthorize("hasAuthority('/ams/admin/read')")
public JsonResult sayHello(){
return JsonResult.ok("可以访问");
}
}
全部完成后,重启项目,使用具有/ams/admin/read权限的用户可以直接访问,不具有此权限的用户则不能访问(将出现403)
使用Knife4j测试,添加一个全局参数,参数名必须是Authorization,参数值就是生成的jwt令牌:



