栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

SpringSecurity单点登录3

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

SpringSecurity单点登录3

续:

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 JsonResult implements 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-starter
   2.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
        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令牌:

 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1008988.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号