zoukankan      html  css  js  c++  java
  • spring boot rest 接口集成 spring security(2)


    Spring Boot 集成教程


    在教程 [spring boot rest 接口集成 spring security(1) - 最简配置] 里介绍了最简集成spring security的过程,本文将继续介绍spring boot项目中集成spring security以及配置jwt的过程。

    如果不了解jwt,可以参考5分钟搞懂:JWT(Json Web Token)

    项目内容

    本文将通过创建一个实际的spring boot项目来演示spring security及jwt的配置过程,项目主要内容:

    • 集成spring security;
    • 配置jwt;
    • 加载用户信息;
    • 实现几个接口,配置访问权限;
    • 最后通过Postman测试接口;

    要求

    • JDK1.8或更新版本
    • Eclipse开发环境

    如没有开发环境,可参考前面章节 [spring boot 开发环境搭建(Eclipse)]。

    项目创建

    创建spring boot项目

    打开Eclipse,创建spring boot的spring starter project项目,选择菜单:File > New > Project ...,弹出对话框,选择:Spring Boot > Spring Starter Project,在配置依赖时,勾选web, security,完成项目创建。

    image

    项目依赖

    要使用jwt,引入jwt jar包

    		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
    		<dependency>
    		    <groupId>io.jsonwebtoken</groupId>
    		    <artifactId>jjwt</artifactId>
    		    <version>0.9.1</version>
    		</dependency>
    

    项目配置

    application.properties配置

    ## 服务器端口,如果不配置默认是8080端口
    server.port=8096 
    
    ## jwt配置
    #  签名密钥
    jwt.secret=my_secret_2019
    # jwt有效期(秒)
    jwt.expiration=1800
    

    代码实现

    项目目录结构如下图,我们添加了几个类,下面将详细介绍。

    image

    spring security的配置:SecurityConfig.java

    这是spring security的java配置类,几个主要的配置:

    • 用户信息加载配置
    • 权限不足处理配置
    • 权限配置
    • jwt过滤器配置
    • 其他如密码加密,CORS等配置
    
    @Configuration
    @EnableWebSecurity // 添加security过滤器
    @EnableGlobalMethodSecurity(prePostEnabled = true) // 可以在controller方法上配置权限
    public class SecurityConfig extends WebSecurityConfigurerAdapter{
        
    	// 加载用户信息
        @Autowired
        private UserDetailsService myUserDetailsService;
        
        // 权限不足错误信息处理,包含认证错误与鉴权错误处理
        @Autowired
        private JwtAuthError myAuthErrorHandler;
        
    	// 密码明文加密方式配置
        @Bean
        public PasswordEncoder myEncoder() {
          return new BCryptPasswordEncoder();
        }
        
        // jwt校验过滤器,从http头部Authorization字段读取token并校验
        @Bean
        public JwtAuthFilter myAuthFilter() throws Exception {
            return new JwtAuthFilter();
        }
        
        // 获取AuthenticationManager(认证管理器),可以在其他地方使用
    	@Bean(name="authenticationManagerBean")
    	@Override
    	public AuthenticationManager authenticationManagerBean() throws Exception {
    		return super.authenticationManagerBean();
    	}
        
        // 认证用户时用户信息加载配置,注入myUserDetailsService
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
        	auth.userDetailsService(myUserDetailsService);
        }
        
        // 配置http,包含权限配置
        @Override
        protected void configure(HttpSecurity http) throws Exception {
        	http
    
        	// 由于使用的是JWT,我们这里不需要csrf
        	.csrf().disable()
    
        	// 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
    
            // 设置myUnauthorizedHandler处理认证失败、鉴权失败
            .exceptionHandling().authenticationEntryPoint(myAuthErrorHandler).accessDeniedHandler(myAuthErrorHandler).and()
    
            // 设置权限
            .authorizeRequests()
    
            // 需要登录
            .antMatchers("/hello/hello1").authenticated()
    
             // 需要角色权限
            .antMatchers("/hello/hello2").hasRole("ADMIN")
    
            // 除上面外的所有请求全部放开
            .anyRequest().permitAll();
    
        	// 添加JWT过滤器,JWT过滤器在用户名密码认证过滤器之前
        	http.addFilterBefore(myAuthFilter(), UsernamePasswordAuthenticationFilter.class);
    
            // 禁用缓存
    //    	http.headers().cacheControl();  
        }
        
        // 配置跨源访问(CORS)
        @Bean
        CorsConfigurationSource corsConfigurationSource() {
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
            return source;
        }
    }
    

    用户信息及用户信息服务:AuthUser.java,AuthUserService.java

    加载用户信息,需要用户信息类及用户信息服务类。AuthUser继承spring的UserDetails,必须重写UserDetails的一些标准接口。注意与实体类User区别。

    
    public class AuthUser implements UserDetails {
    
    	private static final long serialVersionUID = -2336372258701871345L;
    	
    	//用户实体类
    	private User user;
    	
    	public AuthUser(User user) {
    		this.setUser(user);
    	}
    	
    	public static Collection<? extends GrantedAuthority> getAuthoritiesByRole(String role) {
    		Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
    		
    		List<String> roles = Arrays.asList(role.split(","));
    		if (roles.contains("user")) {
    			authorities.add(new SimpleGrantedAuthority("ROLE_USER")); 
    		}
    		if (roles.contains("admin")) {
    			authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); 
    		} 
    
    		return authorities;
    	}
    	
    	// 提供权限信息
    	@Override
    	public Collection<? extends GrantedAuthority> getAuthorities() {
    
    		return getAuthoritiesByRole(getUser().getRole());
    	}
    
    	// 提供账号名称
    	@Override
    	public String getUsername() {
    		return getUser().getMobile();
    	}
    
    	// 提供密码
    	@Override
    	public String getPassword() {
    		return getUser().getPassword();
    	}
    
    	// 账号是否没过期,过期的用户无法认证
    	@Override
    	public boolean isAccountNonExpired() {
    		return true;
    	}
    
    	// 账号是否没锁住,锁住的用户无法认证
    	@Override
    	public boolean isAccountNonLocked() {
    		return true;
    	}
    
    	// 密码是否没过期,密码过期的用户无法认证
    	@Override
    	public boolean isCredentialsNonExpired() {
    		return true;
    	}
    
    	// 用户是否使能,未使能的用户无法认证
    	@Override
    	public boolean isEnabled() {
    		return true;
    	}
    
    	public User getUser() {
    		return user;
    	}
    
    	public void setUser(User user) {
    		this.user = user;
    	}
    
    }
    

    AuthUserService继承UserDetailsService,重写了加载用户信息接口:

    @Service
    public class AuthUserService implements UserDetailsService {
    
    	// 加载用户信息
    	@Override
    	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    		
    		// 此处应从数据库加载用户信息,为简便起见,直接创建一个用户
    		// password的值:$2a$10$EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG,
    		// 是明文123456使用BCryptPasswordEncoder加密的值
    		User user = new User(1l, "abc1", username, "$2a$10$EmsokMb6Vkav7m61kY0PtO.ZCLe0h.uJqVAZW7YYBpSUxd/DMkZuG", "user");
    		AuthUser authUser = new AuthUser(user);
    		
    		return (UserDetails) authUser;
    	}
    }
    

    认证失败、鉴权失败处理:JwtAuthError.java

    当认证失败,系统会抛出认证失败异常,可以配置我们自己的认证失败处理类,同样鉴权失败也可以配置我们自己的失败处理类。

    JwtAuthError继承AuthenticationEntryPoint(认证失败接口)、AccessDeniedHandler(鉴权失败接口),重写了这2个接口类的失败处理方法,其实JwtAuthError可以分为2个类,我们合二为一了。

    @Component
    public class JwtAuthError implements AuthenticationEntryPoint, AccessDeniedHandler {
    
    	@SuppressWarnings("unused")
    	private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthError.class);
    
    	// 认证失败处理,返回401 json数据
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
        	
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("{"status":401,"message":"Unauthorized or invalid token"}");
        	
        }
        
        // 鉴权失败处理,返回403 json数据
    	@Override
    	public void handle(HttpServletRequest request, HttpServletResponse response,
    			AccessDeniedException accessDeniedException) throws IOException, ServletException {
    		
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("{"status":403,"message":"Forbidden"}");
    	}
    }
    

    JWT过滤器

    JWT过滤器每次请求应该只执行一次,所以继承OncePerRequestFilter,JWT过滤器的主要行为:

    • 对于每次请求,从http头部Authorization字段中读取jwt
    • 尝试解密jwt,如果正常解出,说明是合法用户
    • 如果是合法用户,设置认证信息,认证通过
    
    @Component
    public class JwtAuthFilter extends OncePerRequestFilter {
    
    	private static final org.slf4j.Logger log = LoggerFactory.getLogger(JwtAuthFilter.class);
    
        @Autowired
        private JwtUtil jwtUtil;
    
        private String tokenHeader="Authorization";
    
        private String tokenPrefix="Bearer";
    
        @Override
        protected void doFilterInternal(
                HttpServletRequest request,
                HttpServletResponse response,
                FilterChain chain) throws ServletException, IOException {
        	
        	// 从http头部读取jwt
            String authHeader = request.getHeader(this.tokenHeader);
            if (authHeader != null && authHeader.startsWith(tokenPrefix)) {
    	        
    	        final String authToken = authHeader.substring(tokenPrefix.length() + 1); // The part after "Bearer "
    	        String username = null, role = null;
    	        
    	        // 从jwt中解出账号与角色信息
    	        try {
    	        	username = jwtUtil.getUsernameFromToken(authToken);
    	        	role = jwtUtil.getClaimFromToken(authToken, "role", String.class);
    	        } catch (Exception e) {
    	        	log.debug("异常详情", e);
    	        	log.info("无效token");
    	        }
    	        
    	        // 如果jwt正确解出账号信息,说明是合法用户,设置认证信息,认证通过
    	        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
    	        	
    	            UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
    	            		username, null, AuthUser.getAuthoritiesByRole(role));
    	            
    	            // 把请求的信息设置到UsernamePasswordAuthenticationToken details对象里面,包括发请求的ip等
    	            auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
    	            
    	            // 设置认证信息
    	            SecurityContextHolder.getContext().setAuthentication(auth);
    		        
    	        }
            }
                
            // 调用下一个过滤器
            chain.doFilter(request, response);
        }
    }
    

    User实体类(model层)

    User实体类对应于数据库中的User表(我们简化了,没有连数据库)

    
    public class User {
        private Long id;
    
        private String nickname;
    
        private String mobile;
    
        private String password;
    
        private String role;
    
        public User(Long id, String nickname, String mobile, String password, String role) {
            this.id = id;
            this.nickname = nickname;
            this.mobile = mobile;
            this.password = password;
            this.role = role;
        }
    
        public User() {
            super();
        }
    }
    

    LoginRequest类(model层)

    登录请求类,这个类将会接受并校验用户登录时输入的账号密码,关于输入校验,可以参考 [spring boot输入数据校验(validation)]

    
    public class LoginRequest {
    	
    	@SuppressWarnings("unused")
    	private static final org.slf4j.Logger log = LoggerFactory.getLogger(LoginRequest.class);
    	
    	@NotNull(message="账号必须填")
    	@Pattern(regexp = "^[1]([3][0-9]{1}|59|58|88|89)[0-9]{8}$", message="账号请输入11位手机号") // 手机号
    	private String account;
    	
        @NotNull(message="密码必须填")
        @Size(min=6, max=16, message="密码6~16位")
    	private String password;
        
    	private boolean rememberMe;
    	
    	public String getAccount() {
    		return account;
    	}
    	public void setAccount(String account) {
    		this.account = account;
    	}
    	public String getPassword() {
    		return password;
    	}
    	public void setPassword(String password) {
    		this.password = password;
    	}
    	public boolean isRememberMe() {
    		return rememberMe;
    	}
    	public void setRememberMe(boolean rememberMe) {
    		this.rememberMe = rememberMe;
    	}
    	
    }
    

    AuthController类(控制层)

    AuthController类实现了2个REST API:

    • login - 用户提供账号密码,如果密码正确,返回token,否则返回账号或密码错误提示;
    • refresh 输入一个合法的旧token,返回新token
    
    @RestController
    @RequestMapping("/auth")
    public class AuthController {
    	
        @Autowired
        private AuthService authService;
        
    	/**
    	 * login 
    	 * @param authRequest
    	 * @param bindingResult
    	 * @return ResponseEntity<Result> 
    	 */
        @RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
        public ResponseEntity<Result> login(@Valid @RequestBody LoginRequest authRequest, BindingResult bindingResult) throws AuthenticationException{
        	
    		if(bindingResult.hasErrors()) {			
    			Result res = MiscUtil.getValidateError(bindingResult);
    			return new ResponseEntity<Result>(res, HttpStatus.UNPROCESSABLE_ENTITY);
    		}
        	
            final String token = authService.login(authRequest.getAccount(), authRequest.getPassword());
            
            // Return the token
            Result res = new Result(200, "ok");
            res.putData("token", token);
            return ResponseEntity.ok(res);
        }
        
    	/**
    	 * refresh 
    	 * @param request
    	 * @return ResponseEntity<Result> 
    	 */
        @RequestMapping(value = "/refresh", method = RequestMethod.GET, produces="application/json")
        public ResponseEntity<Result> refresh(HttpServletRequest request, @RequestParam String token) throws AuthenticationException{
        	
        	Result res = new Result(200, "ok");
        	
        	String refreshedToken = authService.refresh(token);
            
            if(refreshedToken == null) {
            	res.setStatus(400);
            	res.setMessage("无效token");
                return new ResponseEntity<Result>(res, HttpStatus.BAD_REQUEST);
            } 
            
            
            res.putData("token", token);
            return ResponseEntity.ok(res);
        }
    	
    }
    

    HelloController类(控制层)

    实现了3个REST API:

    • hello1
    • hello2
    • hello3

    用于测试权限配置

    
    @RestController
    @RequestMapping("/hello")
    public class HelloController {
    	
    	@RequestMapping(value="/hello1", method=RequestMethod.GET)
        public String hello1() {
    	        
            return "Hello1!";
        }
    	
    	@RequestMapping(value="/hello2", method=RequestMethod.GET)
        public String hello2() {
    	        
            return "Hello2!";
        }
    	
    	@RequestMapping(value="/hello3", method=RequestMethod.GET)
        public String hello3() {
    	        
            return "Hello3!";
        }
    }
    

    AuthService接口与AuthServiceImpl实现类(服务层)

    AuthService提供对AuthController的服务

    AuthService.java

    public interface AuthService {
        User register(User userToAdd);
        String login(String username, String password);
        String refresh(String oldToken);
    }
    

    AuthServiceImpl.java

    
    @Service
    public class AuthServiceImpl implements AuthService {
    	
    	private static final org.slf4j.Logger log = LoggerFactory.getLogger(AuthServiceImpl.class);
    
        private AuthenticationManager authenticationManager;
        private UserDetailsService userDetailsService;
        private JwtUtil jwtUtil;
    
        @Autowired
        public AuthServiceImpl(
                AuthenticationManager authenticationManager,
                UserDetailsService userDetailsService,
                JwtUtil jwtUtil) {
            this.authenticationManager = authenticationManager;
            this.userDetailsService = userDetailsService;
            this.jwtUtil = jwtUtil;
        }
    
        @Override
        public User register(User userToAdd) {
        	// TODO: 保存user到数据库
            return null;
        }
    
        @Override
        public String login(String username, String password) {
        	// 认证用户,认证失败抛出异常,由JwtAuthError的commence类返回401
            UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
            final Authentication authentication = authenticationManager.authenticate(upToken);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            
            // 如果认证通过,返回jwt
        	final AuthUser userDetails = (AuthUser) userDetailsService.loadUserByUsername(username);
            final String token = jwtUtil.generateToken(userDetails.getUser());
            return token;
        }
    
        @Override
        public String refresh(String oldToken) {
            String newToken = null;
            
            try {
            	newToken = jwtUtil.refreshToken(oldToken);
            } catch (Exception e) {
            	log.debug("异常详情", e);
            	log.info("无效token");
            }
    		return newToken;
        }
    }
    

    其他

    剩下的一些类

    • Result.java 结果封装类
    • MiscUtil.java 辅助类
    • JwtUtil.java jwt处理类,加密解密等操作

    运行

    Eclipse左侧,在项目根目录上点击鼠标右键弹出菜单,选择:run as -> spring boot app 运行程序。 打开Postman访问接口,运行结果如下:

    访问/hello/hello1接口,需要登录访问,没有带上token,返回401

    image

    登录获取token

    image

    再次访问需要登录访问的/hello/hello1接口,带上token,可以看到访问成功

    image

    访问需要admin权限的/hello/hello2接口,虽然带上token,但权限不足,可以看到返回403

    image

    总结

    完整代码

  • 相关阅读:
    解决url传递过程中加号变空格的问题<转>
    windows下CEF3的关闭流程《转》
    mfc封装cef浏览器 关闭整个窗口程序得时候又重启mfc 应用的程序
    雷神免费资源
    LCA的 RMQ解法模版
    最新的js焦点图库
    whereis+whatis+man
    Anroid 手机助手 详细解析 概述(二)
    <c:forEach varStatus="status">中 varStatus的属性简介
    JBoss 系列四十九:JBoss 7/WildFly 中端口使用列表
  • 原文地址:https://www.cnblogs.com/jinbuqi/p/11021971.html
Copyright © 2011-2022 走看看