Spring-Security+JWT+MongoDB解析Spring-Security的基本流程
前言:
最近再看Spring-Security,通过不断的调试,总算对其流程有所认识,总结一下。这里用到了Security和jwt,都是很实用的知识点,希望能够帮到你。用MongoDB是因为我最近再看,只涉及最简单的用法,想换成mysql也可以。(刚开始写博客,写的不好多多包涵)
看完这篇文章之后你可以知道
- 如何使用springboot,springSecurity,jwt实现基于token的权限管理
整理一下思路:
创建一个新工程时,我们需要思考一下我们接下来需要的一些步骤,需要做什么,怎么做。
-
搭建springboot工程
-
导入springSecurity跟jwt的依赖
-
用户的实体类
-
dao层
-
service层(真正开发时再写,这里就直接调用dao层操作数据库)
-
实现UserDetailsService接口
-
实现UserDetails接口
-
验证用户登录信息的拦截器
-
验证用户权限的拦截器
-
springSecurity配置
-
认证的Controller以及测试的controller
-
测试
-
享受成功的喜悦
创建一个springboot工程
建议使用maven去构建项目。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
实体类User
创建一个演示的实体类User,包含最基本的用户名跟密码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
private ObjectId Id;
private String username;
private String password;
}
JwtUtils
jwt工具类,对jjwt封装一下方便调用
public class JwtUtils {
private static Logger logger = LoggerFactory.getLogger(JwtUtils.class);
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final Long EXPIRE = 24 * 3600 * 1000L; //设置一天时间
private static final String SECRET = "wzomg"; //用于signature(签名)部分解密
//生成token
public static String createJwt(String userId, String username) {
/*setIssuer:设置iss(发行方)索赔
setSubject:设置sub(主题)声明
setAudience:设置aud(受众群体)声明
setExpiration:设置exp(到期时间)声明
setNotBefore:设置nbf(不早于)声明
setIssuedAt:设置iat(签发)声明
setId:设置jti(JWT ID)声明*/
Assert.notNull(userId, "用户ID不能为空");
Assert.notNull(username, "用户名不能为空");
return Jwts.builder().setSubject("userId")
.claim("password", userId)
.claim("username", username)
.setIssuedAt(new Date())
.setId(UUID.randomUUID().toString())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
.signWith(SignatureAlgorithm.HS256, SECRET).compact();
}
//解析token
public static Claims parseJwt(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
// 是否已过期
public static boolean isExpiration(String token){
return getTokenBody(token).getExpiration().before(new Date());
}
private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
public static void main(String[] args) { System.out.println(parseJwt("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI2MzQ2MjlhMzQ2YzM4MjA4Y2FhOGMxMjYiLCJ1c2VySWQiOiI2MzQ2MjlhMzQ2YzM4MjA4Y2FhOGMxMjYiLCJ1c2VybmFtZSI6IjEyMzQ1NjciLCJpYXQiOjE2NjU2MjQzODIsImp0aSI6IjU0YWQwYTUzLTQyOGYtNDBkOC05ODJhLTgwMGQ1MzQ0Nzg4OSIsImV4cCI6MTY2NTcxMDc4Mn0.ZCDeq4gTRWCBIjekCk6uEJ3MyOkRbl25oyA5gtl-PEc"));
}
}
UserDao层
将存储到数据库
public interface UserDao extends MongoRepository<User, ObjectId> {
User findUserByUsername(String username);
}
UserService层
调用dao层(简单的查询,没啥主要代码)
@Service
public class UserService {
@Resource
private MongoTemplate mongoTemplate;
public User findByUsername(User user) {
Query query = new Query();
query.addCriteria(Criteria.where("username").is(user.getUsername()));
query.addCriteria(Criteria.where("password").is(user.getPassword()));
return mongoTemplate.findOne(query, User.class);
}
}
JwtAuthUser
封装了用户信息,用于认证和鉴权。
public class JwtAuthUser extends User implements UserDetails {
public JwtAuthUser() {
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return new ArrayList<>();
}
@Override
public String getPassword() {
return super.getPassword();
}
@Override
public String getUsername() {
return super.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
//这里不能写成这样,因为判定账号是否锁定需要特定的逻辑放在认证成功的地方进行处理
//return !super.getStatus().equals(ConstValueEnum.ACCOUNT_FREEZED);
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
配置拦截器
JwtLoginAuthFilter
JwtLoginAuthFilter继承于UsernamePasswordAuthenticationFilter
该拦截器用于获取用户登录的信息,只需创建一个token并调用authenticationManager.authenticate()让spring-security去进行验证就可以了,不用自己查数据库再对比密码了,这一步交给spring去操作。
public class JwtLoginAuthFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtLoginAuthFilter(AuthenticationManager authenticationManager) {
//super(authenticationManager);
this.authenticationManager = authenticationManager;
this.setFilterProcessesUrl("/user/login");
}
//登录验证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
User loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
System.out.println("user:"+loginUser.toString());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
//return super.attemptAuthentication(request, response);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
// 所以就是JwtUser啦
User jwtUser = (User) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
String token = JwtUtils.createJwt(jwtUser.getPassword(),jwtUser.getUsername());
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的格式应该是 `Bearer token`
response.setHeader("token", JwtUtils.TOKEN_PREFIX + token);
}
// 这是验证失败时候调用的方法
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
JWTAuthorizationFilter
验证成功当然就是进行鉴权了,每一次需要权限的请求都需要检查该用户是否有该权限去操作该资源,当然这也是框架帮我们做的,那么我们需要做什么呢?很简单,只要告诉spring-security该用户是否已登录,是什么角色,拥有什么权限就可以了。这里练习我没有做鉴权,使用这个类主要为了分发token,熟悉Security的基本流程。
JWTAuthenticationFilter继承于BasicAuthenticationFilter,至于为什么要继承这个我也不太清楚了,这个我也是网上看到的其中一种实现,实在springSecurity苦手,不过我觉得不继承这个也没事呢(实现以下filter接口或者继承其他filter实现子类也可以吧)只要确保过滤器的顺序,JWTAuthorizationFilter在JWTAuthenticationFilter后面就没问题了。(在SpringSecurity里配置)
public class JwtLoginAuthFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
public JwtLoginAuthFilter(AuthenticationManager authenticationManager) {
//super(authenticationManager);
this.authenticationManager = authenticationManager;
//设置登录路径
this.setFilterProcessesUrl("/user/login");
}
//登录验证
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
User loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
System.out.println("user:"+loginUser.toString());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
e.printStackTrace();
return null;
}
//return super.attemptAuthentication(request, response);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 查看源代码会发现调用getPrincipal()方法会返回一个实现了`UserDetails`接口的对象
// 所以就是JwtUser啦
User jwtUser = (User) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
String token = JwtUtils.createJwt(jwtUser.getPassword(),jwtUser.getUsername());
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的格式应该是 `Bearer token`
response.setHeader("token", JwtUtils.TOKEN_PREFIX + token);
}
// 这是验证失败时候调用的方法
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
配置SecurityConfig
到这里基本操作都写好啦,现在就需要我们将这些辛苦写好的“组件”组合到一起发挥作用了,那就需要配置了。需要开启一下注解@EnableWebSecurity
然后再继承一下WebSecurityConfigurerAdapter
就可以啦 (@EnableWebSecurity注解有两个作用,1: 加载了WebSecurityConfiguration配置类, 配置安全认证策略。2: 加载了AuthenticationConfiguration, 配置了认证信息。 )
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService;
//加密器
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
auth.userDetailsService(userDetailsService)
//.passwordEncoder(new BCryptPasswordEncoder());
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
@Override
public void configure(WebSecurity web) throws Exception {
//super.configure(web);
web.ignoring().antMatchers("/static/","/login.html"
//,"/user/login"
,"/js/**"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.cors().and().csrf().disable()
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests()
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
//设置拦截器,顺序不能反
.addFilter(new JwtLoginAuthFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.httpBasic().authenticationEntryPoint(new UnAuthEntryPoint()); //没有权限访问;
}
}
剩余代码
UserController
@Controller
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/login")
public String login(@RequestBody User user) {
return "success";
}
}
TaskController
@RestController
@RequestMapping("/tasks")
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+"的任务";
}
}
UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userDao.findUserByUsername(s);
// System.out.println("查询到的登录用户信息为:" + user);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在!");
}
JwtAuthUser jwtAuthUser = new JwtAuthUser();
BeanUtils.copyProperties(user, jwtAuthUser);
return jwtAuthUser;
}
}
UnAuthEntryPoint
//未认证时执行这个类
public class UnAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
System.out.println("没权限!!!!!!!");
}
}
UserService
@Service
public class UserService {
@Resource
private MongoTemplate mongoTemplate;
public User findByUsername(User user) {
Query query = new Query();
query.addCriteria(Criteria.where("username").is(user.getUsername()));
query.addCriteria(Criteria.where("password").is(user.getPassword()));
return mongoTemplate.findOne(query, User.class);
}
}
验证结果
验证登录
我事先数据库配好了账号密码
设置请求体和请求头(以json数据传输即可)
验证
响应头中有token,说明成功了
验证登录后
登录成功后,根据SecurityConfig里的
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
我们可以访问/tasks下的路径,当然要设置一下token。只需要把该响应头添加到我们的请求头上去,这里需要把Bearer[空格]
去掉,注意Bearer后的空格也要去掉,因为postman再选了BearerToken之后会自动在token前面再加一个Bearer
验证
没啥问题。
梳理一下Spring-Security的基本流程
给DefaultSecurityFilterChain类的这个方法打断点
我们可以看到Spring-Security的基本流程其中5,6,7是我们重点实现的
-
JwtLoginAuthFilter继承了UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。其中attemptAuthentication方法校验用户名和密码,successfulAuthentication方法是校验成功后执行的。我在successfulAuthentication中生成了token并设置到了请求头中。
-
JWTAuthorizationFilter继承了BasicAuthenticationFilter
此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头部信息。生成token后,会自动解析
错误总结
- o.s.s.c.a.web.builders.WebSecurity : You are asking Spring Security to ignore Ant [pattern=‘/user/login’]. This is not recommended – please use permitAll via HttpSecurity#authorizeHttpRequests instead.
因为在JwtLoginAuthFilter设置了this.setFilterProcessesUrl(“/user/login”);而且在SecurityConfig无需过滤直接访问中也加了/user/login,所以我们访问localhost:8080/user/login会直接请求UserController层,去掉SecurityConfig里的/user/login即可。
- Encoded password does not look like BCrypt
密码未加密,在SecurityConfig设置auth.passwordEncoder(NoOpPasswordEncoder.getInstance());即可但这个方法不建议使用,我这个只是测试,正常写项目肯定是要加密的。
- 我一开始是想用前端页面测试,无奈本人前端是个小趴菜,我在前端遇到的主要问题是无法将表单数据以JSON数据传输到后端。如果你会,我的resource下有页面,直接修改即可。
- 还有其他零零碎碎的问题,建议一步一步debug,理清关系,就可以掌握基本的spring-security啦。
享受成功的喜悦
总的写下来,还是感觉自己掌握的不熟练,如果哪里有不足或者出现错误可以告诉一下我,或者可以到GitHub上提个issue一起讨论下。
代码地址
gitee地址:security_demo1
主要参考博客:老哥写的真真不错