本次整合实现的目标:1、SSO单点登录2、基于角色和spring security注解的权限控制。

整合过程如下:

1、使用maven构建项目,加入先关依赖,pom.xml如下:

4.0.0
powerx.io
springboot-security-jwt
0.0.1-SNAPSHOT
jar
springboot-security-jwt
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
2.0.5.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.7.0
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-maven-plugin

2、JWT相关的两个过滤器:认证过滤器JWTLoginFilter和鉴权过滤器JWTAuthenticationFilter,为了简单,这里没有写专门的JWTUtils工具类来进行token的一系列操作,只是简单的利用JWT的第三方jar生成了token。

JWTLoginFilter.java

packagecom.example.demo;importjava.io.IOException;importjava.io.PrintWriter;importjava.util.ArrayList;importjava.util.Date;importjava.util.stream.Collectors;importjavax.servlet.FilterChain;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;importio.jsonwebtoken.Claims;importio.jsonwebtoken.Jwts;importio.jsonwebtoken.SignatureAlgorithm;/*** 验证用户名密码正确后,生成一个token,并将token返回给客户端
* 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 ,
* attemptAuthentication:接收并解析用户凭证。
* successfulAuthentication:用户成功登录后,这个方法会被调用,我们在这个方法里生成token并返回。
**/
public class JWTLoginFilter extendsUsernamePasswordAuthenticationFilter {privateAuthenticationManager authenticationManager;publicJWTLoginFilter(AuthenticationManager authenticationManager) {this.authenticationManager =authenticationManager;
}
@OverridepublicAuthentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)throwsAuthenticationException {
String username= request.getParameter("username");
String password= request.getParameter("password");return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password, new ArrayList<>()));
}
@Overrideprotected voidsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication auth)throwsIOException, ServletException {
Claims claims=Jwts.claims();
claims.put("role", auth.getAuthorities().stream().map(s ->s.getAuthority()).collect(Collectors.toList()));
String token=Jwts.builder()
.setClaims(claims)
.setSubject(auth.getName())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 24 * 1000))
.signWith(SignatureAlgorithm.HS512,"MyJwtSecret11").compact();
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
String str= "{\"token\":\"" + token + "\"}";
PrintWriter out;try{
out=response.getWriter();
out.print(str);
out.flush();
out.close();
}catch(IOException e) {
e.printStackTrace();
}
}
}
JWTAuthenticationFilter.java
packagecom.example.demo;importjava.io.IOException;importjava.util.List;importjava.util.stream.Collectors;importjavax.servlet.FilterChain;importjavax.servlet.ServletException;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importorg.springframework.security.authentication.AuthenticationManager;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.context.SecurityContextHolder;importorg.springframework.security.web.authentication.www.BasicAuthenticationFilter;importio.jsonwebtoken.Claims;importio.jsonwebtoken.Jwts;public class JWTAuthenticationFilter extendsBasicAuthenticationFilter {publicJWTAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);
}/*** 在拦截器中获取token并解析,拿到用户信息,放置到SecurityContextHolder,这样便完成了springsecurity和jwt的整合。*/@Overrideprotected voiddoFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)throwsIOException, ServletException {
String header= request.getHeader("Authorization");if (header == null || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);return;
}
UsernamePasswordAuthenticationToken authentication=getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}privateUsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token= request.getHeader("Authorization");if (token != null) {
Claims claims= Jwts.parser().setSigningKey("MyJwtSecret11").parseClaimsJws(token.replace("Bearer ", ""))
.getBody();
String user=claims.getSubject();
@SuppressWarnings("unchecked")
List roles = claims.get("role", List.class);
List auth = roles.stream().map(s -> newSimpleGrantedAuthority(s)).collect(Collectors.toList());if (user != null) {return new UsernamePasswordAuthenticationToken(user, null, auth);
}return null;
}return null;
}
}

3、spring security基础配置和测试的controller。

WebSecurityConfig.java
packagecom.example.demo;importjava.util.ArrayList;importjava.util.Collection;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;importorg.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configuration.EnableWebSecurity;importorg.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;importorg.springframework.security.core.GrantedAuthority;importorg.springframework.security.core.authority.SimpleGrantedAuthority;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.password.PasswordEncoder;importorg.springframework.security.provisioning.InMemoryUserDetailsManager;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled= true)public class WebSecurityConfig extendsWebSecurityConfigurerAdapter {
@Autowiredprivate MyAuthenticationProvider provider;//自定义的AuthenticationProvider
@BeanpublicPasswordEncoder myPasswordEncoder() {return new MyPasswordEncoder();//自定义的加密工具
}
@Overrideprotected void configure(HttpSecurity http) throwsException {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().loginProcessingUrl("/login")
.and()
.csrf().disable()
.addFilter(newJWTLoginFilter(authenticationManager()))
.addFilter(newJWTAuthenticationFilter(authenticationManager()));
}
@Overridepublic void configure(AuthenticationManagerBuilder auth) throwsException {
auth.authenticationProvider(provider);
auth.userDetailsService(userDetailsService());
}
@BeanpublicUserDetailsService userDetailsService() {
InMemoryUserDetailsManager iud= newInMemoryUserDetailsManager();
Collection adminAuth = new ArrayList<>();
adminAuth.add(new SimpleGrantedAuthority("ROLE_ADMIN"));iud.createUser(new User("zhangsan", "123456", adminAuth));returniud;
}
}
MyAuthenticationProvider.java
packagecom.example.demo;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.security.authentication.AuthenticationProvider;importorg.springframework.security.authentication.UsernamePasswordAuthenticationToken;importorg.springframework.security.core.Authentication;importorg.springframework.security.core.AuthenticationException;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.stereotype.Component;
@Componentpublic class MyAuthenticationProvider implementsAuthenticationProvider {
@AutowiredprivateUserDetailsService userDetailsService;/*** 自定义验证方式*/@Overridepublic Authentication authenticate(Authentication authentication) throwsAuthenticationException {
String username=authentication.getName();
String password=(String) authentication.getCredentials();
UserDetails user=userDetailsService.loadUserByUsername(username);if(user.getPassword().equals(password)) {return new UsernamePasswordAuthenticationToken(username, null, user.getAuthorities());
}return null;
}
@Overridepublic boolean supports(Class>arg0) {return true;
}
}
MyPasswordEncoder.java
packagecom.example.demo;importorg.springframework.security.crypto.password.PasswordEncoder;public class MyPasswordEncoder implementsPasswordEncoder {
@OverridepublicString encode(CharSequence charSequence) {returncharSequence.toString();
}
@Overridepublic booleanmatches(CharSequence charSequence, String s) {returns.equals(charSequence.toString());
}
}
UserController.java
packagecom.example.demo;importorg.springframework.security.access.prepost.PreAuthorize;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;
@RestControllerpublic classUserController {
@RequestMapping("/hello")publicString hello() {return "hello";
}
@PreAuthorize("hasAuthority('ROLE_USER')")
@RequestMapping("/test1")publicString test1() {return "test1";
}
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
@RequestMapping("/test2")publicString test2() {return "test2";
}
}

至此,核心代码都以经贴上,启动项目,使用postman访问http://localhost:8099/login?username=zhangsan&password=123456,响应如下:

discuz java 单点登录 java jwt单点登录_ide

我们访问http://localhost:8099/hello,同时在请求头上加入auth,如下:

discuz java 单点登录 java jwt单点登录_java_02

同样的url,不加auth则会给我们返回默认的登录页,如下:

discuz java 单点登录 java jwt单点登录_spring_03

访问http://localhost:8099/test1,则提示权限不够,因为我们只为zhangsan用户分配了ROLE_ADMIN角色,而test1需要ROLE_USER。如下:

discuz java 单点登录 java jwt单点登录_java_04

访问http://localhost:8099/test2,如下:

discuz java 单点登录 java jwt单点登录_java jwt 单点登录_05

为了验证是否实现了单点登录,我们可以复制我们的项目,使用postman在第一个项目中拿到token,发现这个token在第二个项目中也可以使用,也就证明了我们的项目可以实现单点登录功能,在实际开发中,我们只需要在一个项目中去颁发token,而其他的项目中去按照相同的规则去解析这个token,这样就可以实现单点登录,需要注意的就是在解析token时的签名密码要一致或者对应。

此外说一下,我在写demo中遇到的坑,第一个就是springboot2.0版本以上必须要有一个加密工具的实现,不然会报错;第二个就是在加入我们自己的过滤器后,必须使用默认的登录url即/login,不支持自定义的登录url了,具体原因我也不清楚;第三个就是在生成token时加入角色信息,不能直接加入auth.getAuthorities(),不然在解析时会出现类型不匹配,具体可参考我的代码。